Como programar en Java - 7ma Edicion | Ramon Jimenez Granados

November 19, 2016 | Author: Anonymous | Category: Java
Share Embed


Short Description

Apr 29, 2008 - El ejemplo práctico ayuda a preparar a los estudiantes para los tipos ..... de Herramientas de Concurren...

Description

4/29/08

9:47 AM

Page 1

®





Una introducción completa y autorizada del código activo de DEITEL® a la programación orientada a objetos, con la nueva edición Java™ Standard Edition 6, JDBC™ 4, JavaServer Faces y Servicios Web ¡Java™ es el lenguaje de programación orientada a objetos más popular, con cinco millones de desarrolladores! ™

Esta nueva edición del libro de texto sobre Java más utilizado en el mundo emplea un método anticipado para las clases y objetos. Incluye también una cobertura completa de la programación orientada a objetos en Java, para lo cual presenta varios ejemplos prácticos integrados: la clase Tiempo, la clase Empleado, la clase LibroCalificaciones, un ejemplo práctico opcional de DOO/UML™ 2 con el ATM (capítulos 1 a 8 y 10), el ejemplo práctico opcional de GUI y gráficos (capítulos 3 a 10), un libro de direcciones controlado por base de datos (capítulo 25) y dos aplicaciones Web multinivel controladas por bases de datos: una libreta de direcciones que utiliza controles JSF habilitados para AJAX para mostrar un nombre y una dirección en un Mapa de Google™ (capítulo 27), y un sistema de reservaciones de una aerolínea que utiliza servicios Web (capítulo 28). Los recursos para los usuarios de este libro incluyen los sitios Web (www.deitel.com y www.pearsoeducacion.net/deitel) con los ejemplos de código del libro e información para profesores, estudiantes y profesionales.

H o

ss F l

y

El CD de este libro incluye material adicional en español y códigos de los ejemplos del libro. Para mayor información visite: www.pearsoneducacion.net/deitel

YE CD-R LU

OM

INC

Deitel Java.qxp

ISBN 978-970-26-1190-5

Visítenos en: www.pearsoneducacion.net

®

www.elsolucionario.net

www.elsolucionario.net



www.elsolucionario.net

www.elsolucionario.net



P. J. Deitel Deitel & Associates, Inc.

H. M. Deitel Deitel & Associates, Inc. TRADUCCIÓN

Alfonso Vidal Romero Elizondo Ingeniero en Sistemas Electrónicos Instituto Tecnológico y de Estudios Superiores de Monterrey Campus Monterrey REVISIÓN TÉCNICA

Gabriela Azucena Campos García Roberto Martínez Román Departamento de Computación Instituto Tecnológico y de Estudios Superiores de Monterrey Campus Estado de México

Jorge Armando Aparicio Lemus Coordinador del Área de Software Universidad Tecnológica de El Salvador

www.elsolucionario.net

DEITEL, PAUL J. Y HARVEY M. DEITEL CÓMO PROGRAMAR EN JAVA. Séptima edición PEARSON EDUCACIÓN, México 2008 ISBN: 978-970-26-1190-5 Área: Computación Formato: 20 × 25.5 cm

Páginas: 1152

Authorized translation from the English language edition entitled Java™ How to Program, 7th Edition, by Deitel & Associates (Harvey & Paul), published by Pearson Education, Inc., publishing as Prentice Hall, Inc., Copyright © 2007. All rights reserved. ISBN 0-13-222220-5 Traducción autorizada de la edición en idioma inglés titulada Java™ How to Program, 7a Edición, por Deitel & Associates (Harvey & Paul), publicada por Pearson Education, Inc., publicada como Prentice Hall, Inc., Copyright © 2007. Todos los derechos reservados. Esta edición en español es la única autorizada. Edición en español Editor: Editor de desarrollo: Supervisor de producción:

Luis Miguel Cruz Castillo e-mail: luis.cruzpearsoned.com Bernardino Gutiérrez Hernández Enrique Trejo Hernández

Edición en inglés Vice President and Editorial Director, ECS: Marcia J. Horton Associate Editor: Jennifer Cappello Assistant Editor: Carole Snyder Executive Managing Editor: Vince O’Brien Managing Editor: Bob Engelhardt Production Editors: Donna M. Crilly, Marta Samsel Director of Creative Services: Paul Belfanti A/V Production Editor: Xiaohong Zhu Art Studio: Artworks, York, PA

Creative Director: Juan López Art Director: Kristine Carney Cover Design: Abbey S. Deitel, Harvey M. Deitel, Francesco Santalucia, Kristine Carney Interior Design: Harvey M. Deitel, Kristine Carney Manufacturing Manager: Alexis Heydt-Long Manufacturing Buyer: Lisa McDowell Executive Marketing Manager: Robin O’Brien

SÉPTIMA EDICIÓN, 2008 D.R. © 2008 por Pearson Educación de México, S.A. de C.V. Atlacomulco 500-5o. piso Col. Industrial Atoto 53519, Naucalpan de Juárez, Estado de México Cámara Nacional de la Industria Editorial Mexicana. Reg. Núm. 1031. Prentice Hall es una marca registrada de Pearson Educación de México, S.A. de C.V. Reservados todos los derechos. Ni la totalidad ni parte de esta publicación pueden reproducirse, registrarse o transmitirse, por un sistema de recuperación de información, en ninguna forma ni por ningún medio, sea electrónico, mecánico, fotoquímico, magnético o electroóptico, por fotocopia, grabación o cualquier otro, sin permiso previo por escrito del editor. El préstamo, alquiler o cualquier otra forma de cesión de uso de este ejemplar requerirá también la autorización del editor o de sus representantes. ISBN 10: 970-26-1190-3 ISBN 13: 978-970-26-1190-5 Impreso en México. Printed in Mexico. 1 2 3 4 5 6 7 8 9 0 - 11 10 09 08

®

www.elsolucionario.net

A Vince O’Brien, Director de Administración de Proyectos, Prentice Hall. Es un privilegio para nosotros trabajar con un profesional consumado. Nuestros mejores deseos para tu éxito continuo.

Paul y Harvey

www.elsolucionario.net

Marcas registradas DEITEL, el insecto con dos pulgares hacia arriba y DIVE INTO son marcas registradas de Deitel and Associates, Inc. Java y todas las marcas basadas en Java son marcas registradas de Sun Microsystems, Inc., en los Estados Unidos y otros países. Pearson Education es independiente de Sun Microsystems, Inc. Microsoft, Internet Explorer y el logotipo de Windows son marcas registradas de Microsoft Corporation en los Estados Unidos y/o en otros países UNIX es una marca registrada de The Open Group.

www.elsolucionario.net

Contenido Prefacio

xix

Antes de empezar

xxx

1

Introducción a las computadoras, Internet y Web

1

1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.10 1.11 1.12 1.13 1.14 1.15 1.16 1.17 1.18 1.19 1.20

Introducción ¿Qué es una computadora? Organización de una computadora Los primeros sistemas operativos Computación personal, distribuida y cliente/servidor Internet y World Wide Web Lenguajes máquina, ensambladores y de alto nivel Historia de C y C++ Historia de Java Bibliotecas de clases de Java FORTRAN, COBOL, Pascal y Ada BASIC, Visual Basic, Visual C++, C# y .NET Entorno de desarrollo típico en Java Generalidades acerca de Java y este libro Prueba de una aplicación en Java Ejemplo práctico de Ingeniería de Software: introducción a la tecnología de objetos y UML Web 2.0 Tecnologías de software Conclusión Recursos Web

2 4 4 5 5 6 6 7 8 8 9 10 10 13 14 19 23 24 25 25

2

Introducción a las aplicaciones en Java

2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 2.10

Introducción Su primer programa en Java: imprimir una línea de texto Modificación de nuestro primer programa en Java Cómo mostrar texto con printf Otra aplicación en Java: suma de enteros Conceptos acerca de la memoria Aritmética Toma de decisiones: operadores de igualdad y relacionales (Opcional) Ejemplo práctico de Ingeniería de Software: cómo examinar el documento de requerimientos de un problema Conclusión

3

Introducción a las clases y los objetos

3.1 3.2

Introducción Clases, objetos, métodos y variables de instancia

www.elsolucionario.net

34 35 35 41 43 44 48 49 52 56 65

75 76 76

viii 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10

Contenido

3.11

Declaración de una clase con un método e instanciamiento de un objeto de una clase Declaración de un método con un parámetro Variables de instancia, métodos establecer y métodos obtener Comparación entre tipos primitivos y tipos por referencia Inicialización de objetos mediante constructores Números de punto flotante y el tipo double (Opcional) Ejemplo práctico de GUI y gráficos: uso de cuadros de diálogo (Opcional) Ejemplo práctico de Ingeniería de Software: identificación de las clases en un documento de requerimientos Conclusión

4

Instrucciones de control: parte 1

4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9 4.10 4.11 4.12 4.13 4.14 4.15 4.16

Introducción Algoritmos Seudocódigo Estructuras de control Instrucción de selección simple if Instrucción de selección doble if...else Instrucción de repetición while Cómo formular algoritmos: repetición controlada por un contador Cómo formular algoritmos: repetición controlada por un centinela Cómo formular algoritmos: instrucciones de control anidadas Operadores de asignación compuestos Operadores de incremento y decremento Tipos primitivos (Opcional) Ejemplo práctico de GUI y gráficos: creación de dibujos simples (Opcional) Ejemplo práctico de Ingeniería de Software: identificación de los atributos de las clases Conclusión

5

Instrucciones de control: parte 2

5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 5.9 5.10 5.11 5.12

Introducción Fundamentos de la repetición controlada por contador Instrucción de repetición for Ejemplos sobre el uso de la instrucción for Instrucción de repetición do...while Instrucción de selección múltiple switch Instrucciones break y continue Operadores lógicos Resumen sobre programación estructurada (Opcional) Ejemplo práctico de GUI y gráficos: dibujo de rectángulos y óvalos (Opcional) Ejemplo práctico de Ingeniería de Software: cómo identificar los estados y actividades de los objetos Conclusión

6

Métodos: un análisis más detallado

211

6.1 6.2 6.3 6.4 6.5 6.6 6.7

Introducción Módulos de programas en Java Métodos static, campos static y la clase Math Declaración de métodos con múltiples parámetros Notas acerca de cómo declarar y utilizar los métodos Pila de llamadas a los métodos y registros de activación Promoción y conversión de argumentos

212 212 214 216 219 221 221

www.elsolucionario.net

77 81 84 88 89 91 95 98 105

112 113 113 114 114 116 117 121 123 127 134 138 139 142 142 146 150

164 165 165 167 171 174 176 183 185 190 194 197 200

Contenido 6.8 6.9

6.15

Paquetes de la API de Java Ejemplo práctico: generación de números aleatorios 6.9.1 Escalamiento y desplazamiento generalizados de números aleatorios 6.9.2 Repetitividad de números aleatorios para prueba y depuración Ejemplo práctico: un juego de probabilidad (introducción a las enumeraciones) Alcance de las declaraciones Sobrecarga de métodos (Opcional) Ejemplo práctico de GUI y gráficos: colores y figuras rellenas (Opcional) Ejemplo práctico de Ingeniería de Software: identificación de las operaciones de las clases Conclusión

7

Arreglos

7.1 7.2 7.3 7.4 7.5 7.6 7.7 7.8 7.9 7.10 7.11 7.12 7.13 7.14 7.15

Introducción Arreglos Declaración y creación de arreglos Ejemplos acerca del uso de los arreglos Ejemplo práctico: simulación para barajar y repartir cartas Instrucción for mejorada Paso de arreglos a los métodos Ejemplo práctico: la clase LibroCalificaciones que usa un arreglo para almacenar las calificaciones Arreglos multidimensionales Ejemplo práctico: la clase LibroCalificaciones que usa un arreglo bidimensional Listas de argumentos de longitud variable Uso de argumentos de línea de comandos (Opcional) Ejemplo práctico de GUI y gráficos: cómo dibujar arcos (Opcional) Ejemplo práctico de Ingeniería de Software: colaboración entre los objetos Conclusión

8

Clases y objetos: un análisis más detallado

8.1 8.2 8.3 8.4 8.5 8.6 8.7 8.8 8.9 8.10 8.11 8.12 8.13 8.14 8.15 8.16 8.17 8.18 8.19

Introducción Ejemplo práctico de la clase Tiempo Control del acceso a los miembros Referencias a los miembros del objeto actual mediante this Ejemplo práctico de la clase Tiempo: constructores sobrecargados Constructores predeterminados y sin argumentos Observaciones acerca de los métodos Establecer y Obtener Composición Enumeraciones Recolección de basura y el método finalize Miembros de clase static Declaración static import Variables de instancia final Reutilización de software Abstracción de datos y encapsulamiento Ejemplo práctico de la clase Tiempo: creación de paquetes Acceso a paquetes (Opcional) Ejemplo práctico de GUI y gráficos: uso de objetos con gráficos (Opcional) Ejemplo práctico de Ingeniería de Software: inicio de la programación de las clases del sistema ATM Conclusión

6.10 6.11 6.12 6.13 6.14

8.20

ix 222 224 227 228 228 232 235 238 241 246

260

www.elsolucionario.net

261 261 262 264 272 274 276 279 284 288 293 294 296 299 305

325 326 327 330 331 333 338 338 340 342 345 345 350 351 353 354 355 360 361 364 369

x

Contenido

9

Programación orientada a objetos: herencia

9.1 9.2 9.3 9.4

Introducción Superclases y subclases Miembros protected Relación entre las superclases y las subclases 9.4.1 Creación y uso de una clase EmpleadoPorComision 9.4.2 Creación de una clase EmpleadoBaseMasComision sin usar la herencia 9.4.3 Creación de una jerarquía de herencia EmpleadoPorComisionEmpleadoBaseMasComision

9.5 9.6 9.7 9.8 9.9

La jerarquía de herencia EmpleadoPorComision-EmpleadoBaseMasComision mediante el uso de variables de instancia protected 9.4.5 La jerarquía de herencia EmpleadoPorComision-EmpleadoBaseMasComision mediante el uso de variables de instancia private Los constructores en las subclases Ingeniería de software mediante la herencia La clase object (Opcional) Ejemplo práctico de GUI y gráficos: mostar texto e imágenes usando etiquetas Conclusión

10

Programación orientada a objetos: polimorfismo

378 379 380 382 382 383 387 391

9.4.4

10.1 10.2 10.3 10.4 10.5

Introducción Ejemplos del polimorfismo Demostración del comportamiento polimórfico Clases y métodos abstractos Ejemplo práctico: sistema de nómina utilizando polimorfismo 10.5.1 Creación de la superclase abstracta Empleado 10.5.2 Creación de la subclase concreta EmpleadoAsalariado 10.5.3 Creación de la subclase concreta EmpleadoPorHoras 10.5.4 Creación de la subclase concreta EmpleadoPorComision 10.5.5 Creación de la subclase concreta indirecta EmpleadoBaseMasComision 10.5.6 Demostración del procesamiento polimórfico, el operador instanceof y la conversión descendente 10.5.7 Resumen de las asignaciones permitidas entre variables de la superclase y de la subclase 10.6 Métodos y clases final 10.7 Ejemplo práctico: creación y uso de interfaces 10.7.1 Desarrollo de una jerarquía PorPagar 10.7.2 Declaración de la interfaz PorPagar 10.7.3 Creación de la clase Factura 10.7.4 Modificación de la clase Empleado para implementar la interfaz PorPagar 10.7.5 Modificación de la clase EmpleadoAsalariado para usarla en la jerarquía PorPagar 10.7.6 Uso de la interfaz PorPagar para procesar objetos Factura y Empleado mediante el polimorfismo 10.7.7 Declaración de constantes con interfaces 10.7.8 Interfaces comunes de la API de Java 10.8 (Opcional) Ejemplo práctico de GUI y gráficos: realizar dibujos mediante el polimorfismo 10.9 (Opcional) Ejemplo práctico de Ingeniería de Software: incorporación de la herencia en el sistema ATM 10.10 Conclusión

www.elsolucionario.net

394 399 404 409 410 411 413

417 418 419 420 423 425 426 426 429 431 432 433 437 438 439 440 441 441 443 445 446 448 448 449 451 457

Contenido

11

Componentes de la GUI: parte 1

11.1 11.2 11.3 11.4 11.5 11.6 11.7 11.8 11.9

Introducción Entrada/salida simple basada en GUI con JOptionPane Generalidades de los componentes de Swing Mostrar texto e imágenes en una ventana Campos de texto y una introducción al manejo de eventos con clases anidadas Tipos de eventos comunes de la GUI e interfaces de escucha Cómo funciona el manejo de eventos

12

Gráficos y Java 2D™

12.1 12.2 12.3 12.4 12.5 12.6 12.7 12.8 12.9

Introducción Contextos y objetos de gráficos Control de colores Control de tipos de letra Dibujo de líneas, rectángulos y óvalos Dibujo de arcos Dibujo de polígonos y polilíneas La API Java 2D Conclusión

13

Manejo de excepciones

13.1 13.2 13.3 13.4 13.5 13.6 13.7 13.8 13.9 13.10 13.11 13.12

Introducción Generalidades acerca del manejo de excepciones Ejemplo: división entre cero sin manejo de excepciones Ejemplo: manejo de excepciones tipo ArithmeticException e InputMismatchException Cuándo utilizar el manejo de excepciones Jerarquía de excepciones en Java Bloque finally Limpieza de la pila printStackTrace, getStackTrace y getMessage Excepciones encadenadas Declaración de nuevos tipos de excepciones Precondiciones y poscondiciones

JButton

Botones que mantienen el estado 11.9.1 JCheckBox 11.9.2 JRadioButton 11.10 JComboBox y el uso de una clase interna anónima para el manejo de eventos 11.11 JList 11.12 Listas de selección múltiple 11.13 Manejo de eventos de ratón 11.14 Clases adaptadoras 11.15 Subclase de JPanel para dibujar con el ratón 11.16 Manejo de eventos de teclas 11.17 Administradores de esquemas 11.17.1 FlowLayout 11.17.2 BorderLayout 11.17.3 GridLayout 11.18 Uso de paneles para administrar esquemas más complejos 11.19 JTextArea 11.20 Conclusión

xi

462 463 464 467 469 474 479 481 483 486 486 489 492 495 497 500 504 507 510 513 514 517 520 522 523 526

539

www.elsolucionario.net

540 542 542 548 554 558 560 563 569

578 579 580 580 582 587 587 590 594 595 597 599 600

xii

Contenido

13.13 Aserciones 13.14 Conclusión

601 602

14

Archivos y flujos

14.1 14.2 14.3 14.4 14.5

14.7 14.8 14.9

Introducción Jerarquía de datos Archivos y flujos La clase File Archivos de texto de acceso secuencial 14.5.1 Creación de un archivo de texto de acceso secuencial 14.5.2 Cómo leer datos de un archivo de texto de acceso secuencial 14.5.3 Ejemplo práctico: un programa de solicitud de crédito 14.5.4 Actualización de archivos de acceso secuencial Serialización de objetos 14.6.1 Creación de un archivo de acceso secuencial mediante el uso de la serialización de objetos 14.6.2 Lectura y deserialización de datos de un archivo de acceso secuencial Clases adicionales de java.io Abrir archivos con JFileChooser Conclusión

15

Recursividad

15.1 15.2 15.3 15.4 15.5 15.6 15.7 15.8 15.9 15.10 15.11

Introducción Conceptos de recursividad Ejemplo de uso de recursividad: factoriales Ejemplo de uso de recursividad: serie de Fibonacci La recursividad y la pila de llamadas a métodos Comparación entre recursividad e iteración Las torres de Hanoi Fractales “Vuelta atrás” recursiva (backtracking) Conclusión Recursos en Internet y Web

16

Búsqueda y ordenamiento

16.1 16.2

16.4 16.5

Introducción Algoritmos de búsqueda 16.2.1 Búsqueda lineal 16.2.2 Búsqueda binaria Algoritmos de ordenamiento 16.3.1 Ordenamiento por selección 16.3.2 Ordenamiento por inserción 16.3.3 Ordenamiento por combinación Invariantes Conclusión

17

Estructuras de datos

17.1 17.2 17.3

Introducción Clases de envoltura de tipos para los tipos primitivos Autoboxing y autounboxing

14.6

16.3

608 609 610 611 613 617 617 623 625 630 630 631 636 638 640 643

653 654 655 655 658 661 662 664 666 676 676 676

685 686 687 687 690 695 695 699 702 708 709

714

www.elsolucionario.net

715 716 716

Contenido 17.4 17.5 17.6 17.7 17.8 17.9 17.10

Clases autorreferenciadas Asignación dinámica de memoria Listas enlazadas Pilas Colas Árboles Conclusión

18

Genéricos

717 717 718 726 730 733 739

761

18.1 18.2 18.3 18.4

Introducción Motivación para los métodos genéricos Métodos genéricos: implementación y traducción en tiempo de compilación Cuestiones adicionales sobre la traducción en tiempo de compilación: métodos que utilizan un parámetro de tipo como tipo de valor de retorno 18.5 Sobrecarga de métodos genéricos 18.6 Clases genéricas 18.7 Tipos crudos (raw) 18.8 Comodines en métodos que aceptan parámetros de tipo 18.9 Genéricos y herencia: observaciones 18.10 Conclusión 18.11 Recursos en Internet y Web

19

Colecciones

19.1 19.2 19.3 19.4 19.5

19.7 19.8 19.9 19.10 19.11 19.12 19.13 19.14 19.15

Introducción Generalidades acerca de las colecciones La clase Arrays La interfaz Collection y la clase Collections Listas 19.5.1 ArrayList e Iterator 19.5.2 LinkedList 19.5.3 Vector Algoritmos de las colecciones 19.6.1 El algoritmo sort 19.6.2 El algoritmo shuffle 19.6.3 Los algoritmos reverse, fill, copy, max y min 19.6.4 El algoritmo binarySearch 19.6.5 Los algoritmos addAll, frequency y disjoint La clase Stack del paquete java.util La clase PriorityQueue y la interfaz Queue Conjuntos Mapas La clase Properties Colecciones sincronizadas Colecciones no modificables Implementaciones abstractas Conclusión

20

Introducción a los applets de Java

20.1 20.2 20.3

Introducción Applets de muestra incluidos en el JDK Applet simple en Java: cómo dibujar una cadena

19.6

xiii

762 762 764 767 770 770 779 783 787 787 787

792

www.elsolucionario.net

793 794 794 797 798 799 800 805 808 809 812 815 816 818 820 822 823 826 829 832 833 834 834

841 842 842 846

xiv

Contenido

20.4 20.5 20.6 20.7 20.8

20.3.1 Cómo ejecutar un applet en el appletviewer 20.3.2 Ejecución de un applet en un explorador Web Métodos del ciclo de vida de los applets Cómo inicializar una variable de instancia con el método int Modelo de seguridad “caja de arena” Recursos en Internet y Web Conclusión

21

Multimedia: applets y aplicaciones

21.1 21.2 21.3 21.4 21.5 21.6 21.7 21.8

Introducción Cómo cargar, mostrar y escalar imágenes Animación de una serie de imágenes Mapas de imágenes Carga y reproducción de clips de audio Reproducción de video y otros medios con el Marco de trabajo de medios de Java Conclusión Recursos Web

22

Componentes de la GUI: parte 2

22.1 22.2 22.3 22.4 22.5 22.6 22.7 22.8 22.9 22.10

Introducción

Apariencia visual adaptable JDesktopPane y JInternalFrame JTabbedPane Administradores de esquemas: BoxLayout y GridBagLayout Conclusión

23

Subprocesamiento múltiple

23.1 23.2 23.3 23.4

Introducción Estados de los subprocesos: ciclo de vida de un subproceso Prioridades y programación de subprocesos Creación y ejecución de subprocesos 23.4.1 Objetos Runnable y la clase Thread 23.4.2 Administración de subprocesos con el marco de trabajo Executor Sincronización de subprocesos 23.5.1 Cómo compartir datos sin sincronización 23.5.2 Cómo compartir datos con sincronización: hacer las operaciones atómicas Relación productor/consumidor sin sincronización Relación productor/consumidor: ArrayBlockingQueue Relación productor/consumidor con sincronización Relación productor/consumidor: búferes delimitados Relación productor/consumidor: las interfaces Lock y Condition Subprocesamiento múltiple con GUIs 23.11.1 Realización de cálculos en un subproceso trabajador 23.11.2 Procesamiento de resultados inmediatos con SwingWorker Otras clases e interfaces en java.util.concurrent Conclusión

23.5 23.6 23.7 23.8 23.9 23.10 23.11 23.12 23.13

JSlider

Ventanas: observaciones adicionales Uso de menús con marcos JPopupMenu

www.elsolucionario.net

848 850 850 851 853 853 854

858 859 860 862 867 869 872 876 876

883 884 884 888 889 896 899 903 906 908 920

925 926 927 929 931 931 934 935 936 940 943 949 952 957 964 970 970 976 982 983

Contenido

24

xv

Redes

992

24.1 24.2 24.3 24.4 24.5 24.6 24.7 24.8

Introducción Manipulación de URLs Cómo leer un archivo en un servidor Web Cómo establecer un servidor simple utilizando sockets de flujo Cómo establecer un cliente simple utilizando sockets de flujo Interacción entre cliente/servidor mediante conexiones de socket de flujo Interacción entre cliente/servidor sin conexión mediante datagramas Juego de Tres en raya (Gato) tipo cliente/servidor, utilizando un servidor con subprocesamiento múltiple 24.9 La seguridad y la red 24.10 [Bono Web] Ejemplo práctico: servidor y cliente DeitelMessenger 24.11 Conclusión

25

Acceso a bases de datos con JDBC

25.1 25.2 25.3 25.4

Introducción Bases de datos relacionales Generalidades acerca de las bases de datos relacionales: la base de datos libros SQL 25.4.1 Consulta básica SELECT 25.4.2 La cláusula WHERE 25.4.3 La cláusula ORDER BY 25.4.4 Cómo fusionar datos de varias tablas: INNER JOIN 25.4.5 La instrucción INSERT 25.4.6 La instrucción UPDATE 25.4.7 La instrucción DELETE Instrucciones para instalar MySQL y MySQL Connector/J Instrucciones para establecer una cuenta de usuario de MySQL Creación de la base de datos libros en MySQL Manipulación de bases de datos con JDBC 25.8.1 Cómo conectarse y realizar consultas en una base de datos 25.8.2 Consultas en la base de datos libros La interfaz RowSet Java DB/Apache Derby Objetos PreparedStatement Procedimientos almacenados Procesamiento de transacciones Conclusión Recursos Web y lecturas recomendadas

25.5 25.6 25.7 25.8 25.9 25.10 25.11 25.12 25.13 25.14 25.15

993 994 998 1001 1003 1004 1014 1021 1034 1034 1035

1041 1042 1043 1044 1047 1047 1048 1050 1051 1053 1053 1054 1055 1056 1057 1057 1057 1062 1073 1075 1076 1090 1091 1091 1092

Los capítulos 26 a 30 así como los apéndices, los encontrará en el CD que acompaña este libro.

26 Aplicaciones Web: parte 1 26.1 26.2 26.3 26.4

Introducción Transacciones HTTP simples Arquitectura de aplicaciones multinivel Tecnologías Web de Java 26.4.1 Servlets 26.4.2 JavaServer Pages 26.4.3 JavaServer Faces 26.4.4 Tecnologías Web en Java Studio Creator 2

www.elsolucionario.net

1101 1102 1103 1105 1106 1106 1106 1107 1108

xvi 26.5

Contenido

26.8 26.9

Creación y ejecución de una aplicación simple en Java Studio Creator 2 26.5.1 Análisis de un archivo JSP 26.5.2 Análisis de un archivo de bean de página 26.5.3 Ciclo de vida del procesamiento de eventos 26.5.4 Relación entre la JSP y los archivos de bean de página 26.5.5 Análisis del XHTML generado por una aplicación Web de Java 26.5.6 Creación de una aplicación Web en Java Studio Creator 2 Componentes JSF 26.6.1 Componentes de texto y gráficos 26.6.2 Validación mediante los componentes de validación y los validadores personalizados Rastreo de sesiones 26.7.1 Cookies 26.7.2 Rastreo de sesiones con el objeto SessionBean Conclusión Recursos Web

27

Aplicaciones Web: parte 2

27.1 27.2

27.6 27.7

Introducción Acceso a bases de datos en las aplicaciones Web 27.2.1 Creación de una aplicación Web que muestra datos de una base de datos 27.2.2 Modificación del archivo de bean de página para la aplicación LibretaDirecciones Componentes JSF habilitados para Ajax 27.3.1 Biblioteca de componentes Java BluePrints Autocomplete Text Field y formularios virtuales 27.4.1 Configuración de los formularios virtuales 27.4.2 Archivo JSP con formularios virtuales y un AutoComplete Text Field 27.4.3 Cómo proporcionar sugerencias para un AutoComplete Text Field Componente Map Viewer de Google Maps 27.5.1 Cómo obtener una clave de la API Google Maps 27.5.2 Cómo agregar un componente y un Map Viewer a una página 27.5.3 Archivo JSP con un componente Map Viewer 27.5.4 Bean de página que muestra un mapa en el componente Map Viewer Conclusión Recursos Web

28

Servicios Web JAX-WS, Web 2.0 y Mash-ups

28.1

Introducción 28.1.1 Descarga, instalación y configuración de Netbeans 5.5 y Sun Java System Application Server 28.1.2 Centro de recursos de servicios Web y Centros de recursos sobre Java en www.deitel.com Fundamentos de los servicios Web de Java Creación, publicación, prueba y descripción de un servicio Web 28.3.1 Creación de un proyecto de aplicación Web y cómo agregar una clase de servicio Web en Netbeans 28.3.2 Definición del servicio Web EnteroEnorme en Netbeans 28.3.3 Publicación del servicio Web EnteroEnorme desde Netbeans 28.3.4 Prueba del servicio Web EnteroEnorme con la página Web Tester de Sun Java System Application Server 28.3.5 Descripción de un servicio Web con el Lenguaje de descripción de servicios Web (WSDL)

26.6 26.7

27.3 27.4

27.5

28.2 28.3

www.elsolucionario.net

1108 1109 1111 1115 1115 1115 1117 1123 1123 1128 1137 1138 1150 1162 1163

1173 1174 1174 1175 1183 1185 1186 1187 1187 1189 1192 1196 1196 1196 1197 1201 1206 1206

1212 1213 1214 1215 1215 1216 1216 1217 1221 1222 1224

Contenido 28.4

Cómo consumir un servicio Web 28.4.1 Creación de un cliente para consumir el servicio Web EnteroEnorme 28.4.2 Cómo consumir el servicio Web EnteroEnorme 28.5 SOAP 28.6 Rastreo de sesiones en los servicios Web 28.6.1 Creación de un servicio Web Blackjack 28.6.2 Cómo consumir el servicio Web Blackjack 28.7 Cómo consumir un servicio Web controlado por base de datos desde una aplicación Web 28.7.1 Configuración de Java DB en Netbeans y creación de la base de datos Reservacion 28.7.2 Creación de una aplicación Web para interactuar con el servicio Web Reservacion 28.8 Cómo pasar un objeto de un tipo definido por el usuario a un servicio Web 28.9 Conclusión 28.10 Recursos Web

29

Salida con formato

29.1 29.2 29.3 29.4 29.5 29.6 29.7 29.8 29.9 29.10 29.11 29.12 29.13 29.14

Introducción Flujos Aplicación de formato a la salida con printf Impresión de enteros Impresión de números de punto flotante Impresión de cadenas y caracteres Impresión de fechas y horas Otros caracteres de conversión Impresión con anchuras de campo y precisiones Uso de banderas en la cadena de formato de printf Impresión con índices como argumentos Impresión de literales y secuencias de escape Aplicación de formato a la salida con la clase Formatter Conclusión

30

Cadenas, caracteres y expresiones regulares

30.1 30.2 30.3

Introducción Fundamentos de los caracteres y las cadenas La clase String 30.3.1 Constructores de String 30.3.2 Métodos length, charAt y getChars de String 30.3.3 Comparación entre cadenas 30.3.4 Localización de caracteres y subcadenas en las cadenas 30.3.5 Extracción de subcadenas de las cadenas 30.3.6 Concatenación de cadenas 30.3.7 Métodos varios de String 30.3.8 Método valueOf de String La clase StringBuilder 30.4.1 Constructores de StringBuilder 30.4.2 Métodos length, capacity, setLength y ensureCapacity de StringBuilder 30.4.3 Métodos charAt, setCharAt, getChars y reverse de StringBuilder 30.4.4 Métodos append de StringBuilder 30.4.5 Métodos de inserción y eliminación de StringBuilder La clase Character La clase StringTokenizer Expresiones regulares, la clase Pattern y la clase Matcher Conclusión

30.4

30.5 30.6 30.7 30.8

xvii 1224 1225 1227 1234 1234 1235 1239 1249 1249 1253 1258 1266 1267

1275

www.elsolucionario.net

1276 1276 1276 1277 1278 1279 1280 1283 1284 1285 1289 1290 1290 1291

1297 1298 1298 1299 1299 1300 1301 1305 1307 1308 1308 1309 1311 1311 1312 1313 1314 1316 1317 1321 1322 1330

xviii

Contenido

A

Tabla de precedencia de los operadores

1340

B

Conjunto de caracteres ASCII

1342

C

Palabras clave y palabras reservadas

1343

D

Tipos primitivos

1344

E

Sistemas numéricos

1345

E.1 E.2 E.3 E.4 E.5 E.6

Introducción Abreviatura de los números binarios como números octales y hexadecimales Conversión de números octales y hexadecimales a binarios Conversión de un número binario, octal o hexadecimal a decimal Conversión de un número decimal a binario, octal o hexadecimal Números binarios negativos: notación de complemento a dos

1346 1348 1349 1350 1351 1352

F

GroupLayout

F.1 F.2 F.3 F.4

Introducción Fundamentos de GroupLayout Creación de un objeto SelectorColores Recursos Web sobre GroupLayout

G

Componentes de integración Java Desktop (JDIC)

G.1 G.2 G.3 G.4 G.5 G.6

Introducción Pantallas de inicio La clase Desktop Iconos de la bandeja Proyectos JDIC Incubator Demos de JDIC

H

Mashups

1374

Índice

1381

1357 1357 1357 1358 1367

1368 1368 1368 1370 1371 1373 1373

www.elsolucionario.net

Prefacio “No vivas más en fragmentos, sólo conéctate”. —Edgar Morgan Foster ¡Bienvenido a Java y Cómo programar en Java, 7ª edición! En Deitel & Associates escribimos para Prentice Hall libros de texto sobre lenguajes de programación y libros de nivel profesional, impartimos capacitación a empresas en todo el mundo y desarrollamos negocios en Internet. Fue un placer escribir esta edición ya que refleja cambios importantes en el lenguaje Java y en las formas de impartir y aprender programación. Se han realizado ajustes considerables en todos los capítulos.

Características nuevas y mejoradas He aquí una lista de las actualizaciones que hemos realizado a la 6ª y 7ª ediciones: •

Actualizamos todo el libro a la nueva plataforma Java Standard Edition 6 (“Mustang”) y lo revisamos cuidadosamente, en base a la Especificación del lenguaje Java.



Revisamos la presentación conforme a las recomendaciones del currículum de ACM/IEEE.



Reforzamos nuestra pedagogía anticipada sobre las clases y los objetos, poniendo especial atención a la orientación de los profesores universitarios en nuestros equipos de revisión, para asegurarnos de obtener el nivel conceptual correcto. Todo el libro está orientado a objetos, y las explicaciones sobre la POO son claras y accesibles. En el capítulo 1 presentamos los conceptos básicos y la terminología de la tecnología de objetos. Los estudiantes desarrollan sus primeras clases y objetos personalizados en el capítulo 3. Al presentar los objetos y las clases en los primeros capítulos, hacemos que los estudiantes “piensen acerca de objetos” de inmediato, y que dominen estos conceptos con más profundidad.



La primera presentación de clases y objetos incluye los ejemplos prácticos de las clases Tiempo, Empleado y LibroCalificaciones, los cuales van haciendo su propio camino a través de varias secciones y capítulos, presentando conceptos de OO cada vez más profundos.



Los profesores que imparten cursos introductorios tienen una amplia opción en cuanto a la cantidad de GUI y gráficos a cubrir; desde cero, a una secuencia introductoria de diez secciones breves, hasta un tratamiento detallado en los capítulos 11, 12 y 22, y en el apéndice F.



Adaptamos nuestra presentación orientada a objetos para utilizar la versión más reciente de UML™ (Lenguaje Unificado de Modelado™): UML™ 2, el lenguaje gráfico estándar en la industria para modelar sistemas orientados a objetos.



En los capítulos 1-8 y 10 presentamos y adaptamos el ejemplo práctico opcional del cajero automático (ATM) de DOO/UML 2. Incluimos un apéndice Web adicional, con la implementación completa del código. Dé un vistazo a los testimonios que se incluyen en la parte posterior del libro.



Agregamos varios ejemplos prácticos sustanciales sobre programación Web orientada a objetos.



Actualizamos el capítulo 25, Acceso a bases de datos con JDBC, para incluir JDBC 4 y utilizar el nuevo sistema de administración de bases de datos Java DB/Apache Derby, además de MySQL. Este capítulo incluye un ejemplo práctico OO sobre el desarrollo de una libreta de direcciones controlada por una base de datos, la cual demuestra las instrucciones preparadas y el descubrimiento automático de controladores de JDBC 4.



Agregamos los capítulos 26 y 27, Aplicaciones Web: partes 1 y 2, que introducen la tecnología JavaServer Faces (JSF) y la utilizan con Sun Java Studio Creador 2 para construir aplicaciones Web de una manera rápida y sencilla. El capítulo 26 incluye ejemplos sobre la creación de GUIs de aplicaciones Web,

www.elsolucionario.net

xx

Prefacio





• •



• • • •

• • • • •

el manejo de eventos, la validación de formularios y el rastreo de sesiones. El material de JSF sustituye los capítulos anteriores sobre servlets y JavaServer Pages (JSP). Agregamos el capítulo 27, Aplicaciones Web: parte 2, que habla acerca del desarrollo de aplicaciones Web habilitadas para Ajax, usando las tecnologías JavaServer Faces y Java BluePrints. Este capítulo incluye una aplicación de libreta de direcciones Web multiniveles, controlada por una base de datos, que permite a los usuarios agregar y buscar contactos, y mostrar las direcciones de los contactos en mapas de Google™ Maps. Esta aplicación habilitada para Ajax le proporciona una sensación real del desarrollo Web 2.0. La aplicación utiliza Componentes JSF habilitados para Ajax para sugerir los nombres de los contactos, mientras el usuario escribe un nombre para localizar y mostrar una dirección localizada en un mapa de Google Maps. Agregamos el capítulo 28, Servicios Web JAX-WS, Web 2.0 y Mash-ups que utiliza un método basado en herramientas para crear y consumir servicios Web, una capacidad típica de Web 2.0. Los ejemplos prácticos incluyen el desarrollo de los servicios Web del juego de blackjack y un sistema de reservaciones de una aerolínea. Utilizamos el nuevo método basado en herramientas para desarrollar aplicaciones Web con rapidez; todas las herramientas pueden descargarse sin costo. Fundamos la Iniciativa Deitel de Negocios por Internet (Deitel Internet Business Initiative) con 60 nuevos centros de recursos para apoyar a nuestros lectores académicos y profesionales. Dé un vistazo a nuestros nuevos centros de recursos (www.deitel.com/resourcecenters.html), incluyendo: Java SE 6 (Mustang), Java, Evaluación y Certificación de Java, Patrones de Diseño de Java, Java EE 5, Motores de Búsqueda de Código y Sitios de Código, Programación de Juegos, Proyectos de Programación y muchos más. Regístrese en el boletín de correo electrónico gratuito Deitel® Buzz Online (www.deitel. com/newsletter/subscribe.html); cada semana anunciamos nuestro(s) centro(s) de recurso(s) más reciente(s); además incluimos otros temas de interés para nuestros lectores. Hablamos sobre los conceptos clave de la comunidad de ingeniería de software, como Web 2.0, Ajax, SOA, servicios Web, software de código fuente abierto, patrones de diseño, mashups, refabricación, programación extrema, desarrollo ágil de software, prototipos rápidos y mucho más. Rediseñamos por completo el capítulo 23, Subprocesamiento múltiple [nuestro agradecimiento especial a Brian Goetz y Joseph Bowbeer, coautores de Java Concurrency in Practice, Addison-Wesley, 2006]. Hablamos sobre la nueva clase SwingWorker para desarrollar interfaces de usuario con subprocesamiento múltiple. Hablamos sobre los nuevos Componentes de Integración de Escritorio de Java (JDIC), como las pantallas de inicio (splash screens) y las interacciones con la bandeja del sistema. Hablamos sobre el nuevo administrador de esquemas GroupLayout en el contexto de la herramienta de diseño de GUI NetBeans 5.5 Matisse para crear GUIs portables que se adhieran a los lineamientos de diseño de GUI de la plataforma subyacente. Presentamos las nuevas características de ordenamiento y filtrado de JTable, que permiten al usuario reordenar los datos en un objeto JTable y filtrarlos mediante expresiones regulares. Presentamos un tratamiento detallado de los genéricos y las colecciones de genéricos. Introducimos los mashups, aplicaciones que, por lo general, se crean mediante llamadas a servicios Web (y/o usando fuentes RSS) de dos o más sitios; otra característica típica de Web 2.0. Hablamos sobre la nueva clase StringBuilder, que tiene un mejor desempeño que StringBuffer en aplicaciones sin subprocesamiento. Presentamos las anotaciones, que reducen en gran parte la cantidad de código necesario para crear aplicaciones.

Las características que se presentan en Cómo programar en Java, 7a edición, incluyen: • •

Cómo obtener entrada con formato mediante la clase Scanner. Mostrar salida con formato mediante el método printf del objeto System.out.

www.elsolucionario.net

Prefacio

xxi



Instrucciones for mejoradas para procesar elementos de arreglos y colecciones.



Declaración de métodos con listas de argumentos de longitud variable (“varargs”).



Uso de clases enum que declaran conjuntos de constantes.



Importación de los miembros static de una clase para usarlos en otra.



Conversión de valores de tipo primitivo a objetos de envolturas de tipo y viceversa, usando autoboxing y auto-unboxing, respectivamente.



Uso de genéricos para crear modelos generales de métodos y clases que pueden declararse una vez, pero usarse con muchos tipos de datos distintos.



Uso de las estructuras de datos mejoradas para genéricos de la API Collections.



Uso de la API Concurrency para implementar aplicaciones con subprocesamiento múltiple.



Uso de objetos RowSet de JDBC para acceder a los datos en una base de datos.

Todo esto ha sido revisado cuidadosamente por distinguidos profesores y desarrolladores de la industria, que trabajaron con nosotros en Cómo programar en Java 6ª y 7ª ediciones. Creemos que este libro y sus materiales de apoyo proporcionarán a los estudiantes y profesionales una experiencia informativa, interesante, retadora y placentera. El libro incluye una extensa suite de materiales complementarios para ayudar a los profesores a maximizar la experiencia de aprendizaje de sus estudiantes. Cómo programar en Java 7ª edición presenta cientos de programas completos y funcionales, y describe sus entradas y salidas. Éste es nuestro característico método de “código activo” (“live code”); presentamos la mayoría de los conceptos de programación de Java en el contexto de programas funcionales completos. Si surge alguna duda o pregunta a medida que lee este libro, envíe un correo electrónico a deitel@deitel. com; le responderemos a la brevedad. Para obtener actualizaciones sobre este libro y el estado de todo el software de soporte de Java, además de las noticias más recientes acerca de todas las publicaciones y servicios de Deitel, visite www.deitel.com. Regístrese en www.deitel.com/newsletter/subscribe.html para obtener el boletín de correo electrónico Deitel® Buzz Online y visite la página www.deitel.com/resourcecenters.html para tener acceso a nuestra lista creciente de centros de recursos.

Uso de UML 2 para desarrollar un diseño orientado a objetos de un ATM. UML 2 se ha convertido en el lenguaje de modelado gráfico preferido para diseñar sistemas orientados a objetos. Todos los diagramas de UML en el libro cumplen con la especificación UML 2. Utilizamos los diagramas de actividad de UML para demostrar el flujo de control en cada una de las instrucciones de control de Java, y usamos los diagramas de clases de UML para representar las clases y sus relaciones de herencia en forma visual. Incluimos un ejemplo práctico opcional (pero altamente recomendado) acerca del diseño orientado a objetos mediante el uso de UML. La revisión del ejemplo práctico estuvo a cargo de un distinguido equipo de profesores y profesionales de la industria relacionados con DOO/UML, incluyendo líderes en el campo de Rational (los creadores de UML) y el Grupo de administración de objetos (responsable de la evolución de UML). En el ejemplo práctico, diseñamos e implementamos por completo el software para un cajero automático (ATM) simple. Las secciones Ejemplo práctico de Ingeniería de Software al final de los capítulos 1 a 8 y 10 presentan una introducción cuidadosamente planeada al diseño orientado a objetos mediante el uso de UML. Presentamos un subconjunto conciso y simplificado de UML 2, y después lo guiamos a través de su primera experiencia de diseño, ideada para los principiantes. El ejemplo práctico no es un ejercicio, sino una experiencia de aprendizaje de principio a fin, que concluye con un recorrido detallado a través del código completo en Java. Las secciones del Ejemplo Práctico de Ingeniería de Software ayudan a los estudiantes a desarrollar un diseño orientado a objetos para complementar los conceptos de programación orientada a objetos que empiezan a aprender en el capítulo 1, y que implementan en el capítulo 3. En la primera de estas secciones, al final del capítulo 1, introducimos los conceptos básicos y la terminología del DOO. En las secciones opcionales Ejemplo Práctico de Ingeniería de Software al final de los capítulos 2 a 5, consideramos cuestiones más sustanciales al emprender la tarea de resolver un problema retador con las técnicas del DOO. Analizamos un documento de requerimientos típico que especifica un sistema a construir, determina los objetos necesarios para implementar ese sistema, establece los atributos que deben tener estos objetos, fija los comportamientos que deben exhibir estos objetos y especifica la forma en que deben interactuar los objetos entre sí para cumplir con los requerimientos del sistema. En un apéndice

www.elsolucionario.net

xxii

Prefacio

Web adicional presentamos el código completo de una implementación en Java del sistema orientado a objetos que diseñamos en los primeros capítulos. El ejemplo práctico ayuda a preparar a los estudiantes para los tipos de proyectos sustanciales que encontrarán en la industria. Empleamos un proceso de diseño orientado a objetos cuidadosamente desarrollado e incremental para producir un modelo en UML 2 para nuestro sistema ATM. A partir de este diseño, producimos una implementación sustancial funcional en Java, usando las nociones clave de la programación orientada a objetos, incluyendo clases, objetos, encapsulamiento, visibilidad, composición, herencia y polimorfismo.

Gráfico de dependencias En el gráfico de la siguiente página se muestra las dependencias entre los capítulos, para ayudar a los profesores a planear su programa de estudios. Cómo programar en Java 7ª edición es un libro extenso, apropiado para una variedad de cursos de programación en distintos niveles. Los capítulos 1-14 forman una secuencia de programación elemental accesible, con una sólida introducción a la programación orientada a objetos. Los capítulos 11, 12, 20, 21 y 22 forman una secuencia sustancial de GUI, gráficos y multimedia. Los capítulos 15 a 19 forman una excelente secuencia de estructuras de datos. Los capítulos 24 a 28 forman una clara secuencia de desarrollo Web con uso intensivo de bases de datos.

Método de enseñanza Cómo programar en Java 7ª edición contiene una extensa colección de ejemplos. El libro se concentra en los principios de la buena ingeniería de software, haciendo hincapié en la claridad de los programas. Enseñamos mediante ejemplos. Somos educadores que impartimos temas de vanguardia en salones de clases de la industria alrededor del mundo. El Dr. Harvey M. Deitel tiene 20 años de experiencia en la enseñanza universitaria y 17, en la enseñanza en la industria. Paul Deitel tiene 15 años de experiencia en la enseñanza en la industria. Juntos han impartido cursos, en todos los niveles, a clientes gubernamentales, industriales, militares y académicos de Deitel & Associates.

Método del código activo. Cómo programar en Java 7ª edición está lleno de ejemplos de “código activo”; esto significa que cada nuevo concepto se presenta en el contexto de una aplicación en Java completa y funcional, que es seguido inmediatamente por una o más ejecuciones actuales, que muestran las entradas y salidas del programa. Este estilo ejemplifica la manera en que enseñamos y escribimos acerca de la programación; a éste le llamamos el método del “código activo”. Resaltado de código. Colocamos rectángulos de color gris alrededor de los segmentos de código clave en cada programa.

Uso de fuentes para dar énfasis. Colocamos los términos clave y la referencia a la página del índice para cada ocurrencia de definición en texto en negritas para facilitar su referencia. Enfatizamos los componentes en pantalla en la fuente Helvética en negritas (por ejemplo, el menú Archivo) y enfatizamos el texto del programa en la fuente Lucida (por ejemplo, int x = 5).

Acceso Web. Todos los ejemplos de código fuente para Cómo programar en Java 7ª edición (y para nuestras otras publicaciones) se pueden descargar en: www.deitel.com/books/jhtp7 www.pearsoneducacion.net/deitel

El registro en el sitio es un proceso fácil y rápido. Descargue todos los ejemplos y, a medida que lea las correspondientes discusiones en el libro de texto, después ejecute cada programa. Realizar modificaciones a los ejemplos y ver los efectos de esos cambios es una excelente manera de mejorar su experiencia de aprendizaje en Java.

Objetivos. Cada capítulo comienza con una declaración de objetivos. Esto le permite saber qué es lo que debe esperar y le brinda la oportunidad, después de leer el capítulo, de determinar si ha cumplido con ellos.

Frases. Después de los objetivos de aprendizaje aparecen una o más frases. Algunas son graciosas, otras filosóficas y las demás ofrecen ideas interesantes. Esperamos que disfrute relacionando las frases con el material del capítulo.

www.elsolucionario.net

Prefacio

xxiii

1 Introducción a las computadoras, Internet y Web 29 Salida con formato (la mayor parte)

2 Introducción a las aplicaciones en Java (Opcional) Ruta de GUI y gráficos 3 Introducción a las clases y los objetos

3.9 Uso de cuadros de diálogo

4 Instrucciones de control: parte 1

4.14 Creación de dibujos simples

5 Instrucciones de control: parte 2

5.10 Dibujo de rectángulos y óvalos

6 Métodos: Un análisis más detallado

6.13 Colores y figuras rellenas

7 Arreglos

7.13 Cómo dibujar arcos

8 Clases y objetos: un análisis más detallado

8.18 Uso de objetos con gráficos

9 Programación orientada a objetos: herencia

9.8 Mostrar texto e imágenes usando etiquetas

10 Programación orientada a objetos: polimorfismo

10.8 Realizar dibujos mediante el polimorfismo

13 Manejo de excepciones1

11 Componentes de la GUI: parte 1

30 Cadenas, caracteres y expresiones regulares

15 Recursividad 3

16 Búsqueda y ordenamiento

17 Estructuras de datos 18 Genéricos 19 Colecciones

14 Archivos y flujos

25 Acceso a base de datos con JDBC 1

12 Gráficos y Java2D™ 20 Introducción a los applets de Java

24 Redes 2 26 Aplicaciones Web: parte 1

23 Subprocesamiento múltiple 4

27 Aplicaciones Web: parte 2 28 Servicios Web JAX-WS, Web 2.0 y Mash-ups

21 Multimedia: applets y aplicaciones 22 Componentes de la GUI: parte 2

1. Los capítulos 13 y 25 dependen del capítulo 11 para la GUI que se utiliza en un ejemplo. 2. El capítulo 24 depende del capítulo 20 para un ejemplo que utiliza un applet. El ejemplo práctico extenso al final de este capítulo depende del capítulo 22 para la GUI y del capítulo 23 para el subprocesamiento múltiple. 3. El capítulo 15 depende de los capítulos 11 y 12 para la GUI y los gráficos que se utilizan en un ejemplo. 4. El capítulo 23 depende del capítulo 11 para la GUI que se utiliza en un ejemplo, y de los capítulos 18-19 para un ejemplo.

Plan general. El plan general de cada capítulo le permite abordar el material de manera ordenada, para poder anticiparse a lo que está por venir y establecer un ritmo cómodo y efectivo de aprendizaje.

Ilustraciones/Figuras. Incluimos una gran cantidad de gráficas, tablas, dibujos lineales, programas y salidas de programa. Modelamos el flujo de control en las instrucciones de control mediante diagramas de actividad en

www.elsolucionario.net

xxiv

Prefacio

UML. Los diagramas de clases de UML modelan los campos, constructores y métodos de las clases. En el ejemplo práctico opcional del ATM de DOO/UML 2 hacemos uso extensivo de seis tipos principales de diagramas en UML.

Tips de programación. Incluimos tips de programación para ayudarle a enfocarse en los aspectos importantes del desarrollo de programas. Estos tips y prácticas representan lo mejor que hemos podido recabar a lo largo de seis décadas combinadas de experiencia en la programación y la enseñanza. Una de nuestras alumnas, estudiante de matemáticas, recientemente nos comentó que siente que este método es similar al de resaltar axiomas, teoremas y corolarios en los libros de matemáticas, ya que proporciona una base sólida sobre la cual se puede construir buen software.

Buena práctica de programación Las buenas prácticas de programación llaman la atención hacia técnicas que le ayudarán a producir programas más claros, comprensibles y fáciles de mantener.

Error común de programación Con frecuencia, los estudiantes tienden a cometer ciertos tipos de errores; al poner atención en estos Errores comunes de programación se reduce la probabilidad de que usted pueda cometerlos.

Tip para prevenir errores Estos tips contienen sugerencias para exponer los errores y eliminarlos de sus programas; muchos de ellos describen aspectos de Java que evitan que los errores entren a los programas.

Tip de rendimiento A los estudiantes les gusta “turbo cargar” sus programas. Estos tips resaltan las oportunidades para hacer que sus programas se ejecuten más rápido, o para minimizar la cantidad de memoria que ocupan.

Tip de portabilidad Incluimos Tips de portabilidad para ayudarle a escribir el código que pueda ejecutarse en una variedad de plataformas, y que expliquen cómo es que Java logra su alto grado de portabilidad.

Observación de ingeniería de software Las Observaciones de ingeniería de software resaltan los asuntos de arquitectura y diseño, lo cual afecta la construcción de los sistemas de software, especialmente los de gran escala. Archivo Nuevo Abrir... Cerrar

Observaciones de apariencia visual Le ofrecemos Observaciones de apariencia visual para resaltar las convenciones de la interfaz gráfica de usuario. Estas observaciones le ayudan a diseñar interfaces gráficas de usuario atractivas y amigables para el usuario, en conformidad con las normas de la industria.

Sección de conclusión. Cada uno de los capítulos termina con una sección breve de “conclusión”, que recapitula el contenido del capítulo y la transición al siguiente capítulo. Viñetas de resumen. Cada capítulo termina con estrategias pedagógicas adicionales. Presentamos un resumen detallado del capítulo, estilo lista con viñetas, sección por sección. Terminología. Incluimos una lista alfabetizada de los términos importantes definidos en cada capítulo. Ejercicios de autoevaluación y respuestas. Se incluyen diversos ejercicios de autoevaluación con sus respuestas, para que los estudiantes practiquen por su cuenta.

www.elsolucionario.net

Prefacio

xxv

Ejercicios. Cada capítulo concluye con un diverso conjunto de ejercicios, incluyendo recordatorios simples de terminología y conceptos importantes; identificar los errores en muestras de código, escribir instrucciones individuales de programas; escribir pequeñas porciones de métodos y clases en Java; escribir métodos, clases y programas completos; y crear proyectos finales importantes. El extenso número de ejercicios permite a los instructores adaptar sus cursos a las necesidades únicas de sus estudiantes, y variar las asignaciones de los cursos cada semestre. Los profesores pueden usar estos ejercicios para formar tareas, exámenes cortos, exámenes regulares y proyectos finales. [NOTA: No nos escriba para solicitarnos acceso al Centro de Recursos para Instructores. El acceso está limitado estrictamente a profesores universitarios que impartan clases en base al libro. Los profesores sólo pueden obtener acceso a través de los representantes de Pearson Educación]. Asegúrese de revisar nuestro centro de recursos de proyectos de programación (http://www.deitel.com/ ProgrammingProjects/) para obtener muchos ejercicios adicionales y posibilidades de proyectos. Miles de entradas en el índice. Hemos incluido un extenso índice, que es útil, en especial, cuando se utiliza el libro como referencia.

“Doble indexado” de ejemplos de código activo de Java. Para cada programa de código fuente en el libro, indexamos la leyenda de la figura en forma alfabética y como subíndice, bajo “Ejemplos”. Esto facilita encontrar los ejemplos usando las características especiales.

Recursos para el estudiante incluidos en Cómo programar en Java 7ª edición Hay, disponibles a la venta, una variedad de herramientas de desarrollo, pero ninguna de ellas es necesaria para comenzar a trabajar con Java. Escribimos Cómo programar en Java 7ª edición utilizando sólo el nuevo Kit de Desarrollo de Java Standard Edition (JDK), versión 6.0. Puede descargar la versión actual del JDK del sitio Web de Java de Sun: java.sun.com/javase/downloads/index.jsp. Este sitio también contiene las descargas de la documentación del JDK. El CD que se incluye con este libro contienen el Entorno de Desarrollo Integrado (IDE) NetBeans™ 5.5 para desarrollar todo tipo de aplicaciones en Java, y el software Sun Java™ Studio Creator 2 Update 1 para el desarrollo de aplicaciones Web. Se proporciona también una versión en Windows de MySQL® 5.0 Community Edition 5.0.27 y MySQL Connector/J 5.0.4, para el procesamiento de datos que se lleva a cabo en los capítulos 25 a 28. El CD también contiene los ejemplos del libro y una página Web con vínculos al sitio Web de Deitel & Associates, Inc. Puede cargar esta página Web en un explorador Web para obtener un rápido acceso a todos los recursos. Encontrará recursos adicionales y descargas de software en nuestro centro de recursos de Java SE 6 (Mustang), ubicado en: www.deitel.com/JavaSE6Mustang/

Java Multimedia Cyber Classroom 7ª edición Cómo programar en Java 7ª edición incluye multimedia interactiva con mucho audio y basada en Web, complementaria para el libro Java Multimedia Cyber Classroom, 7ª edición, disponible en inglés. Nuestro Ciber salón de clases (Cyber Classroom) basado en Web incluye recorridos con audio de los ejemplos de código de los capítulos 1 a 14, soluciones a casi la mitad de los ejercicios del libro, un manual de laboratorio y mucho más. Para obtener más información acerca del Cyber Classroom basado en Web, visite: www.prenhall.com/deitel/cyberclassroom/

A los estudiantes que utilizan nuestros Ciber salones de clases les gusta su interactividad y capacidades de referencia. Los profesores nos dicen que sus estudiantes disfrutan al utilizar el Ciber salón de clases y, en consecuencia, invierten más tiempo en los cursos, dominando un porcentaje mayor del material que en los cursos que sólo utilizan libros de texto.

www.elsolucionario.net

xxvi

Prefacio

Recursos para el instructor de Cómo programar en Java 7ª edición Cómo programar en Java 7ª edición tiene una gran cantidad de recursos para los profesores. El Centro de Recursos para Instructores de Prentice Hall contiene el Manual de soluciones, con respuestas para la mayoría de los ejercicios al final de cada capítulo, un Archivo de elementos de prueba de preguntas de opción múltiple (aproximadamente dos por cada sección del libro) y diapositivas en PowerPoint® que contienen todo el código y las figuras del texto, además de los elementos en viñetas que sintetizan los puntos clave del libro. Los profesores pueden personalizar las diapositivas. Si usted todavía no es un miembro académico registrado, póngase en contacto con su representante de Pearson Educación. Cabe mencionar que todos estos recursos se encuentran en inglés.

Boletín de correo electrónico gratuito Deitel® Buzz Online

Cada semana, el boletín de correo electrónico Deitel ® Buzz Online anuncia nuestro(s) centro(s) de recursos más reciente(s) e incluye comentarios acerca de las tendencias y desarrollos en la industria, vínculos a artículos y recursos gratuitos de nuestros libros publicados y de las próximas publicaciones, itinerarios de lanzamiento de productos, fe de erratas, retos, anécdotas, información sobre nuestros cursos de capacitación corporativa impartidos por instructores y mucho más. También es una buena forma para que usted se mantenga actualizado acerca de todo lo relacionado con Cómo programar en Java 7ª edición. Para suscribirse, visite la página Web: www.deitel.com/newsletter/subscribe.html

Novedades en Deitel Centros de recursos y la iniciativa de negocios por Internet de Deitel. Hemos creado muchos centros de recursos en línea (en www.deitel.com/resourcecenters.html) para mejorar su experiencia de aprendizaje en Java. Anunciamos nuevos centros de recursos en cada edición del boletín de correo electrónico Deitel® Buzz Online. Aquellos de especial interés para los lectores de este libro incluyen: Java, Certificación en Java, Patrones de Diseño en Java, Java EE 5, Java SE 6, AJAX, Apache, Motores de Búsqueda de Código y Sitios de Código, Eclipse, Programación de Juegos, Mashups, MySQL, Código Abierto, Proyectos de Programación, Web2.0, Web 3.0, Servicios Web y XML. Los centros de recursos de Deitel adicionales incluyen: Programas Afiliados, Servicios de Alerta, ASP.NET, Economía de Atención, Creación de Comunidades Web, C, C++, C#, Juegos de Computadora, DotNetNuke, FireFox, Gadgets, Google AdSense, Google Analytics, Google Base, Google Services, Google Video, Google Web Toolkit, IE7, Iniciativa de Negocios por Internet, Publicidad por Internet, Internet Video, Linux, Microformatos, .NET, Ning, OpenGL, Perl, PHP, Podcasting, Python, Recommender Systems, RSS, Ruby, Motores de Búsqueda, Optimización de Motores de Búsqueda, Skype, Sudoku, Mundos Virtuales, Visual Basic, Wikis, Windows Vista, WinFX y muchos más por venir. Iniciativa de contenido libre. Nos complace ofrecerle artículos de invitados y tutoriales gratuitos, seleccionados de nuestras publicaciones actuales y futuras como parte de nuestra iniciativa de contenido libre. En cada tema del boletín de correo electrónico Deitel® Buzz Online, anunciamos las adiciones más recientes a nuestra biblioteca de contenido libre.

Reconocimientos Uno de los mayores placeres al escribir un libro de texto es el de reconocer el esfuerzo de mucha gente, cuyos nombres quizá no aparezcan en la portada, pero cuyo arduo trabajo, cooperación, amistad y comprensión fue crucial para la elaboración de este libro. Mucha gente en Deitel & Associates, Inc. dedicó largas horas a este proyecto; queremos agradecer en especial a Abbey Deitel y Barbara Deitel. También nos gustaría agradecer a dos participantes de nuestro programa de Pasantía con Honores, que contribuyeron a esta publicación: Megan Shuster, con especialidad en ciencias computacionales en el Swarthmore College, y Henry Klementowicz, con especialidad en ciencias computacionales en la Universidad de Columbia. Nos gustaría mencionar nuevamente a nuestros colegas que realizaron contribuciones importantes a Cómo programar en Java 6ª edición: Andrew B. Goldberg, Jeff Listfield, Su Zhang, Cheryl Yaeger, Jing Hu, Sin Han Lo, John Paul Casiello y Christi Kelsey. Somos afortunados al haber trabajado en este proyecto con un talentoso y dedicado equipo de editores profesionales en Prentice Hall. Apreciamos el extraordinario esfuerzo de Marcia Horton, Directora Editorial de la División de Ingeniería y Ciencias Computacionales de Prentice Hall. Jennifer Cappello y Dolores Mars hicieron

www.elsolucionario.net

Prefacio

xxvii

un excelente trabajo al reclutar el equipo de revisión del libro y administrar el proceso de revisión. Francesco Santalucia (un artista independiente) y Kristine Carney de Prentice Hall hicieron un maravilloso trabajo al diseñar la portada del libro; nosotros proporcionamos el concepto y ellos lo hicieron realidad. Vince O’Brien, Bob Engelhardt, Donna Crilly y Marta Samsel hicieron un extraordinario trabajo al administrar la producción del libro. Deseamos reconocer el esfuerzo de nuestros revisores. Al adherirse a un estrecho itinerario, escrutinizaron el texto y los programas, proporcionando innumerables sugerencias para mejorar la precisión e integridad de la presentación. Apreciamos con sinceridad los esfuerzos de nuestros revisores de post-publicación de la 6ª edición, y nuestros revisores de la 7ª edición:

Revisores de Cómo programar en Java 7ª edición (incluyendo los revisores de la post-publicación de la 6ª edición) Revisores de Sun Microsystems: Lance Andersen (Líder de especificaciones de JDBC/Rowset, Java SE Engineering), Ed Burns, Ludovic Champenois (Servidor de Aplicaciones de Sun para programadores de Java EE con Sun Application Server y herramientas: NetBeans, Studio Enterprise y Studio Creador), James Davidson, Vadiraj Deshpande (Grupo de Integración de Sistemas de Java Enterprise, Sun Microsystems India), Sanjay Dhamankar (Grupo Core Developer Platform), Jesse Glick (Grupo NetBeans), Brian Goetz (autor de Java Concurrency in Practice, Addison-Wesley, 2006), Doug Kohlert (Grupo Web Technologies and Standards), Sandeep Konchady (Organización de Ingeniería de Software de Java), John Morrison (Grupo Portal Server Product de Sun Java System), Winston Prakash, Brandon Taylor (grupo SysNet dentro de la División de Software) y Jayashri Visvanathan (Equipo de Java Studio Creador de Sun Microsystems). Revisores académicos y de la industria: Akram Al-Rawi (Universidad King Faisal), Mark Biamonte (DataDiret), Ayad Boudiab (Escuela Internacional de Choueifat, Líbano), Joe Bowbeer (Mobile App Consulting), Harlan Brewer (Select Engineering Services), Marita Ellixson (Eglin AFB, Universidad Indiana Wesleyan, Facilitador en Jefe), John Goodson (DataDiret), Anne Horton (Lockheed Martin), Terrell Regis Hull (Logicalis Integration Solutions), Clark Richey (RABA Technologies, LLC, Java Sun Champion), Manfred Riem (UTA Interactive, LLC, Java Sun Champion), Karen Tegtmeyer (Model Technologies, Inc.), David Wolf (Universidad Pacific Lutheran) y Hua Yan (Borough of Manhattan Community Collage, City University of New York). Revisores de la post-publicación de Cómo programar en Java 6ª edición: Anne Horton (Lockheed Martin), William Martz (Universidad de Colorado, en Colorado Springs), Bill O’Farrell (IBM), Jeffry Babb (Universidad Virginia Commonwealth), Jeffrey Six (Universidad de Delaware, Instalaciones Adjuntas), Jesse Glick (Sun Microsystems), Karen Tegtmeyer (Model Technologies, Inc.), Kyle Gabhart (L-3 Communications), Marita Ellixson (Eglin AFB, Universidad Indiana Wesleyan, Facilitador en Jefe) y Sean Santry (Consultor independiente).

Revisores de Cómo programar en Java 6ª edición (incluyendo a los revisores de la post-publicación de la 5ª edición) Revisores académicos: Karen Arlien (Colegio Estatal de Bismarck), Ben Blake (Universidad Estatal de Cleveland), Walt Bunch (Universidad Chapman), Marita Ellixson (Eglin AFB/Universidad de Arkansas), Ephrem Eyob (Universidad Estatal de Virginia), Bjorn Foss (Universidad Metropolitana de Florida), Bill Freitas (The Lawrenceville School), Joe Kasprzyk (Colegio Estatal de Salem), Brian Larson (Modesto Junior College), Roberto Lopez-Herrejon (Universidad de Texas en Austin), Dean Mellas (Cerritos College), David Messier (Eastern University), Andy Novobilski (Universidad de Tennessee, Chattanooga), Richard Ord (Universidad de California, San Diego), Gavin Osborne (Saskatchewan Institute of Applied Science & Technology), Donna Reese (Universidad Estatal de Mississippi), Craig Slinkman (Universidad de Texas en Arlington), Sreedhar Thota (Western Iowa Tech Community Collage), Mahendran Velauthapillai (Universidad de Georgetown), Loran Walter (Universidad Tecnológica de Lawrence) y Stephen Weiss (Universidad de Carolina del Norte en Chapel Hill). Revisores de la industria: Butch Anton (Wi-Tech Consulting), Jonathan Bruce (Sun Microsystems, Inc.; Líder de Especificaciones de JCP para JDBC), Gilad Bracha (Sun Microsystems, Inc.; Líder de Especificaciones de JCP para Genéricos), Michael Develle (Consultor independiente), Jonathan Gadzik (Consultor independiente), Brian Goetz (Quiotix Corporation (Miembro del Grupo de Expertos de Especificaciones de Herramientas de Concurrencia de JCP), Anne Horton (AT&T Bell Laboratories), James Huddleston (Consultor independiente), Peter Jones (Sun Microsystems, Inc.), Doug Kohlert (Sun Microsystems, Inc.), Earl LaBatt (Altaworks Corp./Universidad de New Hampshire), Paul Monday (Sun Microsystems, Inc.), Bill O’Farrell (IBM), Cameron Skinner (Embarcadero Technologies, Inc.), Brandon Taylor (Sun Microsystems, Inc.) y Karen Tegtmeyer (Consultor indepen-

www.elsolucionario.net

xxviii

Prefacio

diente). Revisores del ejemplo práctico opcional de DOO/UML: Sinan Si Alhir (Consultor independiente), Gene Ames (Star HRG), Jan Bergandy (Universidad de Massachussetts en Dartmouth), Marita Ellixson (Eglin AFB/Universidad de Arkansas), Jonathan Gadzik (Consultor independiente), Thomas Harder (ITT ESI, Inc.), James Huddleston (Consultor independiente), Terrell Hull (Consultor independiente), Kenneth Hussey (IBM), Joe Kasprzyk (Colegio Estatal de Salem), Dan McCracken (City College of New York), Paul Monday (Sun Microsystems, Inc.), Davyd Norris (Rational Software), Cameron Skinner (Embarcadero Technologies, Inc.), Craig Slinkman (Universidad de Texas en Arlington) y Steve Tockey (Construx Software). Estos profesionales revisaron cada aspecto del libro y realizaron innumerables sugerencias para mejorar la precisión e integridad de la presentación. Bueno ¡ahí lo tiene! Java es un poderoso lenguaje de programación que le ayudará a escribir programas con rapidez y eficiencia. Escala sin problemas hacia el ámbito del desarrollo de sistemas empresariales, para ayudar a las organizaciones a crear sus sistemas de información críticos. A medida que lea el libro, apreciaremos con sinceridad sus comentarios, críticas, correcciones y sugerencias para mejorar el texto. Dirija toda su correspondencia a: [email protected]

Le responderemos oportunamente y publicaremos las correcciones y aclaraciones en nuestro sitio Web, www.deitel.com/books/jHTP7/

¡Esperamos que disfrute aprendiendo con este libro tanto como nosotros disfrutamos el escribirlo! Paul J. Deitel Dr. Harvey M. Deitel Maynard, Massachussets Diciembre del 2006

Acerca de los autores Paul J. Deitel, CEO y Director Técnico de Deitel & Associates, Inc., es egresado del Sloan School of Management del MIT (Massachussets Institute of Technology), en donde estudió Tecnología de la Información. Posee las certificaciones Programador Certificado en Java (Java Certified Programmer) y Desarrollador Certificado en Java (Java Certified Developer), y ha sido designado por Sun Microsystems como Java Champion. A través de Deitel & Associates, Inc., ha impartido cursos en Java, C, C++, C# y Visual Basic a clientes de la industria, incluyendo: IBM, Sun Microsystems, Dell, Lucent Technologies, Fidelity, NASA en el Centro Espacial Kennedy, el National Severe Storm Laboratory, White Sands Missile Range, Rogue Wave Software, Boeing, Stratus, Cambridge Technology Partners, Open Environment Corporation, One Wave, Hyperion Software, Adra Systems, Entergy, CableData Systems, Nortel Networks, Puma, iRobot, Invensys y muchos más. También ha ofrecido conferencias de Java y C++ para la Boston Chapter of the Association for Computing Machinery. Él y su padre, el Dr. Harvey M. Deitel, son autores de los libros de programación más vendidos en el mundo. Dr. Harvey M. Deitel, es Presidente y Consejero de Estrategia de Deitel & Associates, Inc., tiene 45 años de experiencia en el campo de la computación; lo que incluye un amplio trabajo académico y en la industria. El Dr. Deitel tiene una licenciatura y una maestría por el MIT y un doctorado de la Universidad de Boston. Tiene 20 años de experiencia como profesor universitario, la cual incluye un puesto vitalicio y el haber sido presidente del departamento de Ciencias de la computación en el Boston College antes de fundar, con su hijo Paul J. Deitel, Deitel & Associates, Inc. Él y Paul son coautores de varias docenas de libros y paquetes multimedia, y piensan escribir muchos más. Los textos de los Deitel se han ganado el reconocimiento internacional y han sido traducidos al japonés, alemán, ruso, español, chino tradicional, chino simplificado, coreano, francés, polaco, italiano, portugués, griego, urdú y turco. El Dr. Deitel ha impartido cientos de seminarios profesionales para grandes empresas, instituciones académicas, organizaciones gubernamentales y diversos sectores del ejército.

Acerca de Deitel & Associates, Inc. Deitel & Associates, Inc. es una empresa reconocida a nivel mundial, dedicada al entrenamiento corporativo y la creación de contenido, con especialización en lenguajes de programación, tecnología de software para Internet/World Wide Web, educación de tecnología de objetos y desarrollo de negocios por Internet a través de su

www.elsolucionario.net

Prefacio

xxix

Iniciativa de Negocios en Internet. La empresa proporciona cursos, que son impartidos por instructores, sobre la mayoría de los lenguajes y plataformas de programación, como Java, Java Avanzado, C, C++, C#, Visual C++, Visual Basic, XML, Perl, Python, tecnología de objetos y programación en Internet y World Wide Web. Los fundadores de Deitel & Associates, Inc. son el Dr. Harvey M. Deitel y Paul J. Deitel. Sus clientes incluyen muchas de las empresas más grandes del mundo, agencias gubernamentales, sectores del ejército e instituciones académicas. A lo largo de su sociedad editorial de 30 años con Prentice Hall, Deitel & Associates Inc. ha publicado libros de texto de vanguardia sobre programación, libros profesionales, multimedia interactiva en CD como los Cyber Classrooms, Cursos Completos de Capacitación, cursos de capacitación basados en Web y contenido electrónico para los populares sistemas de administración de cursos WebCT, Blackboard y CourseCompass de Pearson. Deitel & Associates, Inc. y los autores pueden ser contactados mediante correo electrónico en: [email protected]

Para conocer más acerca de Deitel & Associates, Inc., sus publicaciones y su currículum mundial de la Serie de Capacitación Corporativa DIVE INTO®, visite: www.deitel.com

y suscríbase al boletín gratuito de correo electrónico, Deitel® Buzz Online, en: www.deitel.com/newsletter/subscribe.html

Puede verificar la lista creciente de Centros de Recursos Deitel en: www.deitel.com/resourcecenters.html

Quienes deseen comprar publicaciones de Deitel pueden hacerlo en: www.deitel.com/books/index.html

Las empresas, el gobierno, las instituciones militares y académicas que deseen realizar pedidos en masa deben hacerlo directamente con Prentice Hall. Para obtener más información, visite: www.prenhall.com/mischtm/support.html#order

www.elsolucionario.net

Antes de empezar Antes de comenzar a utilizar este libro, debe seguir las instrucciones de esta sección para asegurarse que Java esté instalado de manera apropiada en su computadora.

Convenciones de fuentes y nomenclatura Utilizamos varios tipos de letra para diferenciar los componentes en la pantalla (como los nombres de menús y los elementos de los mismos) y el código o los comandos en Java. Nuestra convención es hacer hincapié en los componentes en pantalla en una fuente Helvetica sans-serif en negritas (por ejemplo, el menú Archivo) y enfatizar el código y los comandos de Java en una fuente Lucida sans-serif (por ejemplo, System.out.println()).

Kit de desarrollo de Java Standard Edition (JDK) 6 Los ejemplos en este libro se desarrollaron con el Kit de Desarrollo de Java Standard Edition (JDK) 6. Puede descargar la versión más reciente y su documentación en: java.sun.com/javase/6/download.jsp

Si tiene preguntas, envíe un correo electrónico a [email protected]. Le responderemos en breve.

Requerimientos de software y hardware del sistema •

Procesador Pentium III de 500 MHz (mínimo) o de mayor velocidad; Sun® Java™ Studio Creator 2 Update 1 requiere un procesador Intel Pentium 4 de 1 GHz (o equivalente).



Microsoft Windows Server 2003, Windows XP (con Service Pack 2), Windows 2000 Professional (con Service Pack 4).



Una de las siguientes distribuciones de Linux: Red Hat® Enterprise Linux 3, o Red Hat Fedora Core 3.



Mínimo 512 MB de memoria en RAM; Sun Java Studio Creator 2 Update 1 requiere 1 GB de RAM.



Mínimo 1.5 GB de espacio en disco duro.



Unidad de CD-ROM.



Conexión a Internet.



Explorador Web, Adobe® Acrobat® Reader® y una herramienta para descomprimir archivos zip.

Uso de los CD Los ejemplos para Cómo programar en Java, 7ª edición se encuentran en los CD (Windows y Linux) que se incluyen en este libro. Siga los pasos de la siguiente sección, Cómo copiar los ejemplos del libro del CD, para copiar el directorio de ejemplos apropiado del CD a su disco duro. Le sugerimos trabajar desde su disco duro en lugar de hacerlo desde su unidad de CD por dos razones: 1, los CD son de sólo lectura, por lo que no podrá guardar sus aplicaciones en ellos; 2 es posible acceder a los archivos con mayor rapidez desde un disco duro que de un CD. Los ejemplos del libro también están disponibles para descargarse de: www.deitel.com/books/jhtp7/ www.pearsoneducacion.net/deitel/

La interfaz para el contenido del CD de Microsoft® Windows® está diseñada para iniciarse de manera automática, a través del archivo AUTORUN.EXE. Si no aparece una pantalla de inicio cuando inserte el CD en su

www.elsolucionario.net

Antes de empezar

xxxi

computadora, haga doble clic en el archivo welcome.htm para iniciar la interfaz del CD para el estudiante, o consulte el archivo readme.txt en el CD. Para iniciar la interfaz del CD para Linux, haga doble clic en el archivo welcome.html.

Cómo copiar los ejemplos del libro del CD Las capturas de pantalla de esta sección pueden diferir un poco de lo que usted verá en su computadora, de acuerdo con el sistema operativo y el explorador Web de que disponga. Las instrucciones de los siguientes pasos asumen que está utilizando Microsoft Windows. 1. Insertar el CD. Inserte el CD que se incluye con este libro en la unidad de CD de su computadora. A continuación deberá aparecer de manera automática la página Web welcome.htm (figura 1) en Windows. También puede utilizar el Explorador de Windows para ver el contenido del CD y hacer doble clic en welcome.htm para mostrar esta página. 2. Abrir el directorio del CD-ROM. Haga clic en el vínculo Browse CD Contents (Explorar contenido del CD) (figura 1) para ver el contenido del CD.

Haga clic en el vínculo Browse CD Contents para acceder al contenido del CD

Figura 1 | Página de bienvenida para el CD de Cómo programar en Java. 3. Copiar el directorio ejemplos. Haga clic en el directorio ejemplos (figura 2), después seleccione Copiar. A continuación, use el Explorador de Windows para ver el contenido de su unidad C:. (Tal vez necesite hacer clic en un vínculo para mostrar el contenido de la unidad). Una vez que se muestre el contenido, haga clic en cualquier parte y seleccione la opción Pegar del menú Editar para copiar el directorio ejemplos del CD a su unidad C:. [Nota: guardamos los ejemplos directamente en la unidad C: y hacemos referencia a esta unidad a lo largo del texto. Puede optar por guardar sus archivos en una unidad distinta, con base en la configuración de su computadora, en el laboratorio de su escuela o sus preferencias personales. Si trabaja en un laboratorio de computadoras, consulte con su profesor para obtener más información para confirmar en dónde se deben guardar los ejemplos].

Modificación de la propiedad de sólo lectura de los archivos Los archivos de ejemplo que copió a su computadora desde el CD son de sólo lectura. A continuación eliminará la propiedad de sólo lectura, para poder modificar y ejecutar los ejemplos. 1. Abrir el cuadro de diálogo Propiedades. Haga clic con el botón derecho del ratón en el directorio ejemplos y seleccione Propiedades. A continuación aparecerá el cuadro de diálogo Propiedades de ejemplos (figura 3).

www.elsolucionario.net

xxxii

Antes de empezar

Haga clic con el botón derecho del ratón en el directorio ejemplos

Seleccione Copiar

Figura 2 | Copia del directorio ejemplos.

Figura 3 | Cuadro de diálogo Propiedades de ejemplos.

2. Cambiar la propiedad de sólo lectura. En la sección Atributos de este cuadro de diálogo, haga clic en el botón Sólo lectura para eliminar la marca de verificación (figura 4). Haga clic en Aplicar para aplicar los cambios.

www.elsolucionario.net

Antes de empezar

xxxiii

Desactive el atributo Sólo lectura

Figura 4 | Desactivar la casilla de verificación Sólo lectura. 3. Cambiar la propiedad para todos los archivos. Al hacer clic en Aplicar se mostrará la ventana Confirmar cambios de atributos (figura 5). En esta ventana, haga clic en el botón de opción Aplicar cambios a esta carpeta y a todas las subcarpetas y archivos y haga clic en Aceptar para eliminar la propiedad de sólo lectura para todos los archivos y directorios en el directorio ejemplos.

Haga clic en este botón de opción para eliminar la propiedad Sólo lectura para todos los archivos

Figura 5 | Eliminar la propiedad de sólo lectura para todos los archivos en el directorio ejemplos.

Instalación del Kit de Desarrollo de Java Standard Edition (JDK) Antes de ejecutar las aplicaciones de este libro o de crear sus propias aplicaciones, debe instalar el Kit de Desarrollo de Java Standard Edition (JDK) 6 o una herramienta de desarrollo para Java que soporte a Java SE 6. Puede descargar el JDK 6 y su documentación de java.sun.com/javase/6/download.jsp. Haga clic en el botón » DOWNLOAD para JDK 6. Debe aceptar el acuerdo de licencia antes de descargar. Una vez que acepte el acuerdo, haga clic en el vínculo para el instalador de su plataforma. Guarde el instalador en su disco duro y no olvide en dónde lo guardó. Antes de instalar, lea con cuidado las instrucciones de instalación del JDK para su plataforma, que se encuentran en java.sun.com/javase/6/webnotes/install/index.html. Después de descargar el instalador del JDK, haga doble clic en el programa instalador para empezar a instalarlo. Le recomendamos que acepte todas las opciones de instalación predeterminadas. Si modifica el directorio predeterminado, asegúrese de anotar el nombre y la ubicación exactos que eligió, ya que necesitará esta información más adelante en el proceso de instalación. En Windows, el JDK se coloca, de manera predeterminada, en el siguiente directorio: C:\Archivos de programa\Java\jdk1.6.0

www.elsolucionario.net

xxxiv

Antes de empezar

Establecer la variable de entorno PATH

La variable de entorno PATH en su computadora indica qué directorios debe buscar la computadora cuando intente localizar aplicaciones, como aquellas que le permiten compilar y ejecutar sus aplicaciones en Java (conocidas como javac.exe y java.exe, respectivamente). Ahora aprenderá a establecer la variable de entorno PATH en su computadora para indicar en dónde están instaladas las herramientas del JDK. 1. Abrir el cuadro de diálogo Propiedades del sistema. Haga clic en Inicio > Panel de control > Sistema para mostrar el cuadro de diálogo Propiedades del sistema (figura 6). [Nota: su cuadro de diálogo Propiedades del sistema puede tener una apariencia distinta al que se muestra en la figura 6, dependiendo de la versión de Microsoft Windows. Este cuadro de diálogo específico es de una computadora que ejecuta Microsoft Windows XP. Sin embargo, el que aparece en su computadora podría incluir distinta información]. 2. Abrir el cuadro de diálogo Variables de entorno. Seleccione la ficha Opciones avanzadas de la parte superior del cuadro de diálogo Propiedades del sistema (figura 7). Haga clic en el botón Variables de entorno para desplegar el cuadro de diálogo Variables de entorno (figura 8). 3. Editar la variable PATH. Desplácese por el cuadro Variables del sistema para seleccionar la variable PATH. Haga clic en el botón Modificar. Esto hará que se despliegue el cuadro de diálogo Modificar la variable del sistema (figura 9). 4. Modificar la variable PATH. Coloque el cursor dentro del campo Valor de variable. Use la flecha izquierda para desplazar el cursor hasta el inicio de la lista. Al principio de la lista, escriba el nombre del directorio en el que colocó el JDK, seguido de \bin; (figura 10). Agregue C:\Archivos de programa \Java\jdk1.6.0\bin; a la variable PATH, si eligió el directorio de instalación predeterminado. No coloque espacios antes o después de lo que escriba. No se permiten espacios antes o después de cada valor en una variable de entorno. Haga clic en el botón Aceptar para aplicar sus cambios a la variable PATH. Si no establece la variable PATH de manera correcta, al utilizar las herramientas del JDK recibirá un mensaje como éste: ‘java’ no se reconoce como un comando interno o externo, programa o archivo por lotes ejecutable.

En este caso, regrese al principio de esta sección y vuelva a comprobar sus pasos. Si ha descargado una versión más reciente del JDK, tal vez necesite modificar el nombre del directorio de instalación del JDK en la variable PATH.

Figura 6 | Cuadro de diálogo Propiedades del sistema.

www.elsolucionario.net

Antes de empezar

Seleccione la ficha Opciones avanzadas

Haga clic en el botón Variables de entorno

Figura 7 | Ficha Opciones avanzadas del cuadro de diálogo Propiedades del sistema.

Figura 8 | Cuadro de diálogo Variables de entorno.

Figura 9 | Cuadro de diálogo Modificar la variable del sistema.

Figura 10 | Modificación de la variable PATH.

www.elsolucionario.net

xxxv

xxxvi

Antes de empezar

Establecer la variable de entorno CLASSPATH Si trata de ejecutar un programa en Java y recibe un mensaje como:

Exception in thread “main” java.lang.NoClassDefFoundError: SuClase

entonces su sistema tiene una variable de entorno CLASSPATH que debe modificarse. Para corregir el error anterior, siga los pasos para establecer la variable de entorno PATH, localice la variable CLASSPATH y modifique su valor para que incluya lo siguiente: .;

al principio de su valor (sin espacios antes o después de estos caracteres). Ahora está listo para empezar sus estudios de Java con el libro Cómo programar en Java, 7ª edición. ¡Esperamos que lo disfrute!

www.elsolucionario.net

1 Nuestra vida se malgasta por los detalles… simplificar, simplificar.

Introducción a las computadoras, Internet y Web

—Henry David Thoreau

La principal cualidad del lenguaje es la claridad. —Galen

OBJETIVOS

Mi sublime objetivo deberé llevarlo a cabo a tiempo.

En este capítulo aprenderá a:

—W. S. Gilbert

Tenía un maravilloso talento para empacar estrechamente el pensamiento, haciéndolo portable.

Q

Comprender los conceptos básicos de hardware y software.

Q

Conocer los conceptos básicos de la tecnología de objetos, como las clases, objetos, atributos, comportamientos, encapsulamiento, herencia y polimorfismo.

Q

Familiarizarse con los distintos lenguajes de programación.

Q

Saber qué lenguajes de programación se utilizan más.

Q

Comprender un típico entorno de desarrollo en Java.

Q

Entender el papel de Java en el desarrollo de aplicaciones cliente/servidor distribuidas para Internet y Web.

Q

Conocer la historia de UML: el lenguaje de diseño orientado a objetos estándar en la industria.

Q

Conocer la historia de Internet y World Wide Web.

Q

Probar aplicaciones en Java.

—Thomas B. Macaulay

“¡Caray, creo que de los dos, el intérprete es el más difícil de entender!” —Richard Brinsley Sheridan

El hombre sigue siendo la computadora más extraordinaria de todas. —John F. Kennedy

www.elsolucionario.net

Pla n g e ne r a l

2

Capítulo 1

1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.10 1.11 1.12 1.13 1.14 1.15 1.16 1.17 1.18 1.19 1.20

Introducción a las computadoras, Internet y Web

Introducción ¿Qué es una computadora? Organización de una computadora Los primeros sistemas operativos Computación personal, distribuida y cliente/servidor Internet y World Wide Web Lenguajes máquina, ensambladores y de alto nivel Historia de C y C++ Historia de Java Bibliotecas de clases de Java FORTRAN, COBOL, Pascal y Ada BASIC, Visual Basic, Visual C++, C# y .NET Entorno de desarrollo típico en Java Generalidades acerca de Java y este libro Prueba de una aplicación en Java Ejemplo práctico de Ingeniería de Software: introducción a la tecnología de objetos y UML Web 2.0 Tecnologías de software Conclusión Recursos Web

Resumen | Terminología | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

1.1 Introducción ¡Bienvenido a Java! Hemos trabajado duro para crear lo que pensamos será una experiencia de aprendizaje informativa, divertida y retadora para usted. Java es un poderoso lenguaje de programación, divertido para los principiantes y apropiado para los programadores experimentados que desarrollan sistemas de información de tamaño considerable. Cómo programar en Java, 7ª edición es una herramienta efectiva de aprendizaje para cada una de estas audiencias.

Pedagogía La parte central del libro se enfoca en la claridad de los programas, a través de las técnicas comprobadas de la programación orientada a objetos. Los principiantes aprenderán programación de manera correcta, desde el principio. La presentación es clara, simple y tiene muchas ilustraciones. Incluye cientos de programas completos y funcionales en Java, y muestra la salida que se obtiene al ejecutar estos programas en una computadora. Enseñamos las características de Java en un contexto de programas completos y funcionales; a esto le llamamos el método de código activo (Live-Code™). Los programas de ejemplo están disponibles en el CD que acompaña a este libro. También puede descargarlos de los sitios Web www.deitel.com/books/jhtp7/ o www.pearsoneducacion. net.com/deitel.

Fundamentos Los primeros capítulos presentan los fundamentos de las computadoras, la programación de éstas y el lenguaje de programación Java, con lo cual se provee una base sólida para un análisis más detallado de Java en los capítulos posteriores. Los programadores experimentados tienden a leer los primeros capítulos rápidamente, y descubren que el análisis de Java en los capítulos posteriores es riguroso y retador. La mayoría de las personas están familiarizadas con las emocionantes tareas que realizan las computadoras. Por medio de este libro, usted aprenderá a programar las computadoras para que realicen dichas tareas. El

www.elsolucionario.net

1.1 Introducción

3

software (las instrucciones que usted escribe para indicar a la computadora que realice acciones y tome decisiones) es quien controla a las computadoras (conocidas comúnmente como hardware). Java, desarrollado por Sun Microsystems, es uno de los lenguajes para desarrollo de software más populares en la actualidad.

Java Standard Edition 6 (Java SE 6) y el Kit de Desarrollo de Java 6 (JDK 6) Este libro se basa en la plataforma Java Standard Edition 6 (Java SE 6) de Sun, también conocida como Mustang. Sun ofrece una implementación de Java SE 6, conocida como Kit de Desarrollo de Java (JDK), que incluye las herramientas necesarias para escribir software en Java. Nosotros utilizamos el JDK versión 6.0 para los programas en este libro. Por lo regular, Sun actualiza el JDK para corregir errores: para descargar la versión más reciente del JDK 6, visite java.sun.com/javase/6/download.jsp.

Evolución de la computación y de la programación El uso de las computadoras se está incrementando en casi cualquier campo de trabajo; los costos de se han reducido en forma dramática, debido al rápido desarrollo en la tecnología de hardware y software. Las computadoras que ocupaban grandes habitaciones y que costaban millones de dólares, hace algunas décadas, ahora pueden colocarse en las superficies de chips de silicio más pequeños que una uña, y con un costo de quizá unos cuantos dólares cada uno. Por fortuna, el silicio es uno de los materiales más abundantes en el planeta (es uno de los ingredientes de la tierra). La tecnología de los chips de silicio ha vuelto tan económica a la tecnología de la computación que cientos de millones de computadoras de uso general se encuentran actualmente ayudando a la gente de todo el mundo en: empresas, la industria, el gobierno y en sus vidas. Dicho número podría duplicarse fácilmente en unos cuantos años. A través de los años, muchos programadores aprendieron la metodología conocida como programación estructurada. Usted aprenderá tanto la programación estructurada como la novedosa y excitante metodología de la programación orientada a objetos. ¿Por qué enseñamos ambas? La programación orientada a objetos es la metodología clave utilizada hoy en día por los programadores. Usted creará y trabajará con muchos objetos de software en este libro. Sin embargo, descubrirá que la estructura interna de estos objetos se construye, a menudo, utilizando técnicas de programación estructurada. Además, la lógica requerida para manipular objetos se expresa algunas veces mediante la programación estructurada.

El lenguaje de elección para las aplicaciones en red Java se ha convertido en el lenguaje de elección para implementar aplicaciones basadas en Internet, y software para dispositivos que se comunican a través de una red. Ahora, los estéreos y otros dispositivos en los hogares pueden conectarse entre sí mediante el uso de tecnología Java. ¡En la conferencia JavaOne en mayo del 2006, Sun anunció que había mil millones de teléfonos móviles y dispositivos portátiles habilitados para Java! Java ha evolucionado rápidamente en el ámbito de las aplicaciones de gran escala. Es el lenguaje preferido para satisfacer la mayoría de las necesidades de programación de muchas organizaciones. Java ha evolucionado tan rápidamente que publicamos esta séptima edición de Cómo programar en Java justamente 10 años después de publicar la primera edición. Java ha crecido tanto que cuenta con otras dos ediciones. La edición Java Enterprise Edition (Java EE) está orientada hacia el desarrollo de aplicaciones de red distribuidas, de gran escala, y aplicaciones basadas en Web. La plataforma Java Micro Edition (Java ME) está orientada hacia el desarrollo de aplicaciones para dispositivos pequeños, con memoria limitada, como los teléfonos celulares, radiolocalizadores y PDAs.

Permanezca en contacto con nosotros Está a punto de comenzar una ruta de desafíos y recompensas. Mientras tanto, si desea comunicarse con nosotros, envíenos un correo a [email protected] o explore nuestro sitio Web en www.deitel.com. Le responderemos a la brevedad. Para mantenerse al tanto de los desarrollos con Java en Deitel & Associates, regístrese para recibir nuestro boletín de correo electrónico, Deitel Buzz Online en

®

www.deitel.com/newsletter/subscribe.html

Para obtener material adicional sobre Java, visite nuestra creciente lista de centros de recursos en www.deitel. com/ResourceCenters.html. Esperamos que disfrute aprender con Cómo programar en Java, 7ª edición.

www.elsolucionario.net

4

Capítulo 1

Introducción a las computadoras, Internet y Web

1.2 ¿Qué es una computadora? Una computadora es un dispositivo capaz de realizar cálculos y tomar decisiones lógicas a velocidades de millones (incluso de miles de millones) de veces más rápidas que los humanos. Por ejemplo, muchas de las computadoras personales actuales pueden realizar varios miles de millones de cálculos en un segundo. Una persona con una calculadora podría requerir toda una vida para completar el mismo número de operaciones. (Puntos a considerar: ¿cómo sabría que la persona sumó los números de manera correcta?, ¿cómo sabría que la computadora sumó los números de manera correcta?) ¡Las supercomputadoras actuales más rápidas pueden realizar billones de sumas por segundo! Las computadoras procesan los datos bajo el control de conjuntos de instrucciones llamadas programas de cómputo. Estos programas guían a la computadora a través de conjuntos ordenados de acciones especificadas por gente conocida como programadores de computadoras. Una computadora está compuesta por diversos dispositivos (como teclado, monitor, ratón, discos, memoria, DVD, CD-ROM y unidades de procesamiento) conocidos como hardware. A los programas que se ejecutan en una computadora se les denomina software. Los costos de las piezas de hardware han disminuido de manera espectacular en años recientes, al punto en que las computadoras personales se han convertido en artículos domésticos. En este libro aprenderá métodos comprobados que pueden reducir los costos de desarrollo del software: programación orientada a objetos y (en nuestro Ejemplo práctico de Ingeniería de Software en los capítulos 2-8 y 10) diseño orientado a objetos.

1.3 Organización de una computadora Independientemente de las diferencias en su apariencia física, casi todas las computadoras pueden representarse mediante seis unidades lógicas o secciones: 1. Unidad de entrada. Esta sección “receptora” obtiene información (datos y programas de cómputo) desde diversos dispositivos de entrada y pone esta información a disposición de las otras unidades para que pueda procesarse. La mayoría de la información se introduce a través de los teclados y ratones; también puede introducirse de muchas otras formas, como hablar con su computadora, digitalizar imágenes y desde una red, como Internet. 2. Unidad de salida. Esta sección de “embarque” toma información que ya ha sido procesada por la computadora y la coloca en los diferentes dispositivos de salida, para que esté disponible fuera de la computadora. Hoy en día, la mayoría de la información de salida de las computadoras se despliega en el monitor, se imprime en papel o se utiliza para controlar otros dispositivos. Las computadoras también pueden dar salida a su información a través de redes como Internet. 3. Unidad de memoria. Esta sección de “almacén” de acceso rápido, pero con relativa baja capacidad, retiene la información que se introduce a través de la unidad de entrada, para que esté disponible de manera inmediata para procesarla cuando sea necesario. La unidad de memoria también retiene la información procesada hasta que ésta pueda colocarse en los dispositivos de salida por la unidad de salida. Por lo general, la información en la unidad de memoria se pierde cuando se apaga la computadora. Con frecuencia, a esta unidad de memoria se le llama memoria o memoria primaria. 4. Unidad aritmética y lógica (ALU). Esta sección de “manufactura” es la responsable de realizar cálculos como suma, resta, multiplicación y división. Contiene los mecanismos de decisión que permiten a la computadora hacer cosas como, por ejemplo, comparar dos elementos de la unidad de memoria para determinar si son iguales o no. 5. Unidad central de procesamiento (CPU). Esta sección “administrativa” coordina y supervisa la operación de las demás secciones. La CPU le indica a la unidad de entrada cuándo debe grabarse la información dentro de la de memoria; a la ALU, cuándo debe utilizarse la información de la memoria para los cálculos; y a la unidad de salida, cuándo enviar la información desde la memoria hasta ciertos dispositivos de salida. Muchas de las computadoras actuales contienen múltiples CPUs y, por lo tanto, pueden realizar diversas operaciones de manera simultánea (a estas computadoras se les conoce como multiprocesadores).

www.elsolucionario.net

1.5

Computación personal, distribuida y cliente/servidor

5

6. Unidad de almacenamiento secundario. Ésta es la sección de “almacén” de alta capacidad y de larga duración. Los programas o datos que no se encuentran en ejecución por las otras unidades, normalmente se colocan en dispositivos de almacenamiento secundario (por ejemplo, el disco duro) hasta que son requeridos nuevamente, posiblemente horas, días, meses o incluso años después. El tiempo para acceder a la información en almacenamiento secundario es mucho mayor que el que se necesita para acceder a la información de la memoria principal, pero el costo por unidad de memoria secundaria es mucho menor que el correspondiente a la unidad de memoria principal. Los CDs y DVDs son ejemplos de dispositivos de almacenamiento secundario y pueden contener hasta cientos de millones y miles de millones de caracteres, respectivamente.

1.4 Los primeros sistemas operativos Las primeras computadoras eran capaces de realizar solamente una tarea o trabajo a la vez. A esta forma de operación de la computadora a menudo se le conoce como procesamiento por lotes (batch) de un solo usuario. La computadora ejecuta un solo programa a la vez, mientras procesa los datos en grupos o lotes. En estos primeros sistemas, los usuarios generalmente asignaban sus trabajos a un centro de cómputo que los introducía en paquetes de tarjetas perforadas, y a menudo tenían que esperar horas, o incluso días, antes de que sus resultados impresos regresaran a sus escritorios. El software denominado sistema operativo se desarrolló para facilitar el uso de la computadora. Los primeros sistemas operativos administraban la suave transición entre trabajos, incrementando la cantidad de trabajo, o el flujo de datos, que las computadoras podían procesar. Conforme las computadoras se volvieron más poderosas, se hizo evidente que un proceso por lotes para un solo usuario era ineficiente, debido al tiempo que se malgastaba esperando a que los lentos dispositivos de entrada/salida completaran sus tareas. Se pensó que era posible realizar muchos trabajos o tareas que podrían compartir los recursos de la computadora y lograr un uso más eficiente. A esto se le conoce como multiprogramación; que significa la operación simultánea de muchas tareas que compiten para compartir los recursos de la computadora. Aun con los primeros sistemas operativos con multiprogramación, los usuarios seguían enviando sus tareas en paquetes de tarjetas perforadas y esperaban horas, incluso hasta días, por los resultados. En la década de los sesenta, varios grupos en la industria y en las universidades marcaron la pauta de los sistemas operativos de tiempo compartido. El tiempo compartido es un caso especial de la multiprogramación, ya que los usuarios acceden a la computadora a través de terminales que, por lo general, son dispositivos compuestos por un teclado y un monitor. Puede haber docenas o incluso cientos de usuarios compartiendo la computadora al mismo tiempo. La computadora en realidad no ejecuta los procesos de todos los usuarios a la vez. Lo que hace es ejecutar una pequeña porción del trabajo de un usuario y después procede a dar servicio al siguiente usuario, con la posibilidad de proporcionar el servicio a cada usuario varias veces por segundo. Así, los programas de los usuarios aparentemente se ejecutan de manera simultánea. Una ventaja del tiempo compartido es que el usuario recibe respuestas casi inmediatas a las peticiones.

1.5 Computación personal, distribuida y cliente/servidor En 1977, Apple Computer popularizó el fenómeno de la computación personal. Las computadoras se volvieron sumamente económicas, de manera que la gente pudo adquirirlas para su uso personal o para negocios. En 1981, IBM, el vendedor de computadoras más grande del mundo, introdujo la Computadora Personal (PC) de IBM. Con esto se legitimó rápidamente la computación en las empresas, en la industria y en las organizaciones gubernamentales. Estas computadoras eran unidades “independientes” (la gente transportaba sus discos de un lado a otro para compartir información; a esto se le conoce comúnmente como “sneakernet”). Aunque las primeras computadoras personales no eran lo suficientemente poderosas para compartir el tiempo entre varios usuarios, podían interconectarse mediante redes computacionales, algunas veces a través de líneas telefónicas y otras mediante redes de área local (LANs) dentro de una empresa. Esto derivó en el fenómeno denominado computación distribuida, en donde todos los cálculos informáticos de una empresa, en vez de realizarse estrictamente dentro de un centro de cómputo, se distribuyen mediante redes a los sitios en donde se realiza el trabajo de la empresa. Las computadoras personales eran lo suficientemente poderosas para manejar los requerimientos de cómputo de usuarios individuales, y para manejar las tareas básicas de comunicación que involucraban la transferencia de información entre una computadora y otra, de manera electrónica.

www.elsolucionario.net

6

Capítulo 1

Introducción a las computadoras, Internet y Web

Las computadoras personales actuales son tan poderosas como las máquinas de un millón de dólares de hace apenas unas décadas. Las máquinas de escritorio más poderosas (denominadas estaciones de trabajo) proporcionan a cada usuario enormes capacidades. La información se comparte fácilmente a través de redes de computadoras, en donde algunas computadoras denominadas servidores almacenan datos que pueden ser utilizados por computadoras cliente distribuidas en toda la red, de ahí el término de computación cliente/servidor. Java se está utilizando ampliamente para escribir software para redes de computadoras y para aplicaciones cliente/servidor distribuidas. Los sistemas operativos actuales más populares como Linux, Mac OS X y Microsoft Windows proporcionan el tipo de capacidades que explicamos en esta sección.

1.6 Internet y World Wide Web Internet (una red global de computadoras) tiene sus raíces en la década de 1960; su patrocinio estuvo a cargo del Departamento de Defensa de los Estados Unidos. Diseñada originalmente para conectar los sistemas de cómputo principales de aproximadamente una docena de universidades y organizaciones de investigación, actualmente, Internet es utilizada por cientos de millones de computadoras y dispositivos controlados por computadora en todo el mundo. Con la introducción de World Wide Web (que permite a los usuarios de computadora localizar y ver documentos basados en multimedia, sobre casi cualquier tema, a través del ciberespacio), Internet se ha convertido explosivamente en uno de los principales mecanismos de comunicación en todo el mundo. Internet y World Wide Web se encuentran, sin duda, entre las creaciones más importantes y profundas de la humanidad. En el pasado, la mayoría de las aplicaciones de computadora se ejecutaban en equipos que no estaban conectados entre sí. Las aplicaciones de la actualidad pueden diseñarse para intercomunicarse entre computadoras en todo el mundo. Internet mezcla las tecnologías de la computación y las comunicaciones. Facilita nuestro trabajo. Hace que la información esté accesible en forma instantánea y conveniente para todo el mundo. Hace posible que los individuos y negocios pequeños locales obtengan una exposición mundial. Está cambiando la forma en que se hacen los negocios. La gente puede buscar los mejores precios para casi cualquier producto o servicio. Los miembros de las comunidades con intereses especiales pueden mantenerse en contacto unos con otros. Los investigadores pueden estar inmediatamente al tanto de los últimos descubrimientos. Cómo programar en Java, 7ª edición presenta técnicas de programación que permiten a las aplicaciones en Java utilizar Internet y Web para interactuar con otras aplicaciones. Estas técnicas, junto con otras más, permiten a los programadores de Java desarrollar el tipo de aplicaciones distribuidas de nivel empresarial que se utilizan actualmente en la industria. Se pueden escribir aplicaciones en Java para ejecutarse en cualquier tipo de computadora, con lo cual se reduce en gran parte el tiempo y el costo de desarrollo de sistemas. Si a usted le interesa desarrollar aplicaciones que se ejecuten a través de Internet y Web, aprender Java puede ser la clave para que reciba oportunidades retadoras y remuneradoras en su profesión.

1.7 Lenguajes máquina, ensambladores y de alto nivel Los programadores escriben instrucciones en diversos lenguajes de programación, algunos de los cuales comprende directamente la computadora, mientras que otros requieren pasos intermedios de traducción. En la actualidad se utilizan cientos de lenguajes de computación. Éstos se dividen en tres tipos generales: 1. Lenguajes máquina. 2. Lenguajes ensambladores. 3. Lenguajes de alto nivel. Cualquier computadora puede entender de manera directa sólo su propio lenguaje máquina; que es su “lenguaje natural”, y como tal, está definido por el diseño del hardware de dicha computadora. Por lo general, los lenguajes máquina consisten en cadenas de números (que finalmente se reducen a 1s y 0s) que instruyen a las computadoras para realizar sus operaciones más elementales, una a la vez. Los lenguajes máquina son dependientes de la máquina (es decir, un lenguaje máquina en particular puede usarse solamente en un tipo de computadora). Dichos lenguajes son difíciles de comprender para los humanos, el siguiente ejemplo muestra uno de los primeros programas en lenguaje máquina, el cual suma el pago de las horas extras al sueldo base y almacena el resultado en el sueldo bruto:

www.elsolucionario.net

1.8

Historia de C y C++

7

+1300042774 +1400593419 +1200274027

La programación en lenguaje máquina era demasiado lenta y tediosa para la mayoría de los programadores. En vez de utilizar las cadenas de números que las computadoras podían entender directamente, los programadores empezaron a utilizar abreviaturas del inglés para representar las operaciones elementales. Estas abreviaturas formaron la base de los lenguajes ensambladores. Los programas traductores conocidos como ensambladores se desarrollaron para convertir los primeros programas en lenguaje ensamblador a lenguaje máquina, a la velocidad de la computadora. A continuación se muestra un ejemplo de un programa en lenguaje ensamblador, que también suma el pago de las horas extras al sueldo base y almacena el resultado en el sueldo bruto: load add store

sueldobase sueldoextra sueldobruto

Aunque este código es más claro para los humanos, las computadoras no lo pueden entender sino hasta que se traduce en lenguaje máquina. El uso de las computadoras se incrementó rápidamente con la llegada de los lenguajes ensambladores, pero los programadores aún requerían de muchas instrucciones para llevar a cabo incluso hasta las tareas más simples. Para agilizar el proceso de programación se desarrollaron los lenguajes de alto nivel, en donde podían escribirse instrucciones individuales para realizar tareas importantes. Los programas traductores, denominados compiladores, convierten, a lenguaje máquina, los programas que están en lenguaje de alto nivel. Estos últimos permiten a los programadores escribir instrucciones que son muy similares al inglés común, y contienen la notación matemática común. Un programa de nómina escrito en un lenguaje de alto nivel podría contener una instrucción como la siguiente: sueldoBruto = sueldoBase + sueldoExtra

Obviamente, desde el punto de vista del programador, los lenguajes de alto nivel son mucho más recomendables que los lenguajes máquina o ensamblador. C, C++ y los lenguajes .NET de Microsoft (por ejemplo, Visual Basic .NET, Visual C++ .NET y C#) son algunos de los lenguajes de programación de alto nivel que más se utilizan; sin embargo, Java es el más utilizado. El proceso de compilación de un programa escrito en lenguaje de alto nivel a un lenguaje máquina puede tardar un tiempo considerable en la computadora. Los programas intérpretes se desarrollaron para ejecutar programas en lenguaje de alto nivel directamente, aunque con más lentitud. Los intérpretes son populares en los entornos de desarrollo de programas, en los cuales se agregan nuevas características y se corrigen los errores. Una vez que se desarrolla un programa por completo, se puede producir una versión compilada para ejecutarse con la mayor eficiencia. Actualmente se sabe que existen dos formas de traducir un programa en lenguaje de alto nivel a un formato que la computadora pueda entender: compilación e interpretación. Como veremos en la sección 1.13, Java utiliza una mezcla inteligente de estas tecnologías.

1.8 Historia de C y C++ Java evolucionó de C++, el cual evolucionó de C, que a su vez evolucionó de BCPL y B. En 1967, Martin Richards desarrolló BCPL como un lenguaje para escribir software para sistemas operativos y compiladores. Ken Thompson modeló muchas características en su lenguaje B a partir del trabajo de sus contrapartes en BCPL, y utilizó a B para crear las primeras versiones del sistema operativo UNIX, en los laboratorios Bell en 1970. El lenguaje C evolucionó a partir de B, gracias al trabajo de Dennis Ritchie en los laboratorios Bell, y se implementó originalmente en 1972. Inicialmente, se hizo muy popular como lenguaje de desarrollo para el sistema operativo UNIX. En la actualidad, la mayoría del código para los sistemas operativos de propósito general (por ejemplo, los que se encuentran en las computadoras portátiles, de escritorio, estaciones de trabajo y pequeños servidores) se escribe en C o C++. A principios de la década de los ochenta, Bjarne Stroustrup desarrolló una extensión de C en los laboratorios Bell: C++. Este lenguaje proporciona un conjunto de características que “pulen” al lenguaje C pero, lo más impor-

www.elsolucionario.net

8

Capítulo 1

Introducción a las computadoras, Internet y Web

tante es que proporciona la capacidad de una programación orientada a objetos (que describiremos con más detalle en la sección 1.16 y en todo el libro). C++ es un lenguaje híbrido: es posible programar en un estilo parecido a C, en un estilo orientado a objetos, o en ambos. Una revolución se está gestando en la comunidad del software. Escribir software de manera rápida, correcta y económica es aún una meta difícil de alcanzar, en una época en que la demanda de nuevo y más poderoso software se encuentra a la alza. Los objetos, o dicho en forma más precisa (como veremos en la sección 1.16), las clases a partir de las cuales se crean los objetos, son en esencia componentes reutilizables de software. Hay objetos de: fecha, hora, audio, automóvil, personas, etcétera; de hecho, casi cualquier sustantivo puede representarse como objeto de software en términos de atributos (como el nombre, color y tamaño) y comportamientos (como calcular, desplazarse y comunicarse). Los desarrolladores de software están descubriendo que utilizar una metodología de diseño e implementación modular y orientada a objetos puede hacer más productivos a los grupos de desarrollo de software, que mediante las populares técnicas de programación anteriores, como la programación estructurada. Los programas orientados a objetos son, a menudo, más fáciles de entender, corregir y modificar. Java es el lenguaje de programación orientada a objetos que más se utiliza en el mundo.

1.9 Historia de Java La contribución más importante a la fecha, por parte de la revolución del microprocesador, es que hizo posible el desarrollo de las computadoras personales, que ahora suman miles de millones a nivel mundial. Las computadoras personales han tenido un profundo impacto en la vida de las personas, y en la manera en que las empresas realizan y administran su negocio. Los microprocesadores están teniendo un profundo impacto en los dispositivos electrónicos inteligentes para uso doméstico. Al reconocer esto, Sun Microsystems patrocinó en 1991 un proyecto interno de investigación denominado Green, el cual desembocó en el desarrollo de un lenguaje basado en C++ al que su creador, James Gosling, llamó Oak debido a un roble que tenía a la vista desde su ventana en las oficinas de Sun. Posteriormente se descubrió que ya existía un lenguaje de computadora con el mismo nombre. Cuando un grupo de gente de Sun visitó una cafetería local, sugirieron el nombre Java (una variedad de café) y así se quedó. Pero el proyecto Green tuvo algunas dificultades. El mercado para los dispositivos electrónicos inteligentes de uso doméstico no se desarrollaba tan rápido a principios de los noventa como Sun había anticipado. El proyecto corría el riesgo de cancelarse. Pero para su buena fortuna, la popularidad de World Wide Web explotó en 1993 y la gente de Sun se dio cuenta inmediatamente del potencial de Java para agregar contenido dinámico, como interactividad y animaciones, a las páginas Web. Esto trajo nueva vida al proyecto. Sun anunció formalmente a Java en una importante conferencia que tuvo lugar en mayo de 1995. Java generó la atención de la comunidad de negocios debido al fenomenal interés en World Wide Web. En la actualidad, Java se utiliza para desarrollar aplicaciones empresariales a gran escala, para mejorar la funcionalidad de los servidores Web (las computadoras que proporcionan el contenido que vemos en nuestros exploradores Web), para proporcionar aplicaciones para los dispositivos domésticos (como teléfonos celulares, radiolocalizadores y asistentes digitales personales) y para muchos otros propósitos.

1.10 Bibliotecas de clases de Java Los programas en Java constan de varias piezas llamadas clases. Estas clases incluyen piezas llamadas métodos, los cuales realizan tareas y devuelven información cuando completan esas tareas. Los programadores pueden crear cada una de las piezas que necesitan para formar programas en Java. Sin embargo, la mayoría de los programadores en Java aprovechan las ricas colecciones de clases existentes en las bibliotecas de clases de Java, que también se conocen como APIs (Interfaces de programación de aplicaciones) de Java. Por lo tanto, en realidad existen dos fundamentos para conocer el “mundo” de Java. El primero es el lenguaje Java en sí, de manera que usted pueda programar sus propias clases; el segundo son las clases incluidas en las extensas bibliotecas de clases de Java. A lo largo de este libro hablaremos sobre muchas bibliotecas de clases; que proporcionan principalmente los vendedores de compiladores, pero muchas de ellas las proporcionan vendedores de software independientes (ISVs).

Observación de ingeniería de software 1.1 Utilice un método de construcción en bloques para crear programas. Evite reinventar la rueda: use piezas existentes siempre que sea posible. Esta reutilización de software es un beneficio clave de la programación orientada a objetos.

www.elsolucionario.net

1.11

FORTRAN, COBOL, Pascal y Ada

9

Incluimos muchos tips como Observaciones de ingeniería de software a lo largo del texto para explicar los conceptos que afectan y mejoran la arquitectura y calidad de los sistemas de software. También resaltamos otras clases de tips, incluyendo las Buenas prácticas de programación (que le ayudarán a escribir programas más claros, comprensibles, de fácil mantenimiento, y fáciles de probar y depurar; es decir, eliminar errores de programación), los Errores comunes de programación (problemas de los que tenemos que cuidarnos y evitar), Tips de rendimiento (que servirán para escribir programas que se ejecuten más rápido y ocupen menos memoria), Tips de portabilidad (técnicas que le ayudarán a escribir programas que se ejecuten, con poca o ninguna modificación, en una variedad de computadoras; estos tips también incluyen observaciones generales acerca de cómo logra Java su alto grado de portabilidad), Tips para prevenir errores (que le ayudarán a eliminar errores de sus programas y, lo que es más importante, técnicas que le ayudarán a escribir programas libres de errores desde el principio) y Observaciones de apariencia visual (que le ayudarán a diseñar la apariencia visual de las interfaces gráficas de usuario de sus aplicaciones, además de facilitar su uso). Muchas de estas técnicas y prácticas son sólo guías. Usted deberá, sin duda, desarrollar su propio estilo de programación.

Observación de ingeniería de software 1.2 Cuando programe en Java, generalmente utilizará los siguientes bloques de construcción: clases y métodos de las bibliotecas de clases, clases y métodos creados por usted mismo, y clases y métodos creados por otros y puestos a disposición suya.

La ventaja de crear sus propias clases y métodos es que sabe exactamente cómo funcionan y puede examinar el código en Java. La desventaja es el tiempo que consumen y el esfuerzo potencialmente complejo que se requiere.

Tip de rendimiento 1.1 Utilizar las clases y métodos de las APIs de Java en vez de escribir sus propias versiones puede mejorar el rendimiento de sus programas, ya que estas clases y métodos están escritos cuidadosamente para funcionar de manera eficiente. Esta técnica también reduce el tiempo de desarrollo de los programas.

Tip de portabilidad 1.1 Utilizar las clases y métodos de las APIs de Java en vez de escribir sus propias versiones mejora la portabilidad de sus programas, ya que estas clases y métodos se incluyen en todas las implementaciones de Java.

Observación de ingeniería de software 1.3 Existen diversas bibliotecas de clases que contienen componentes reutilizables de software, y están disponibles a través de Internet y Web, muchas de ellas en forma gratuita.

Para descargar la documentación de la API de Java, visite el sitio java.sun.com/javase/6/download.jsp de Sun para Java.

1.11 FORTRAN, COBOL, Pascal y Ada Se han desarrollado cientos de lenguajes de alto nivel, pero sólo unos cuantos han logrado una amplia aceptación. Fortran (FORmula TRANslator, Traductor de fórmulas) fue desarrollado por IBM Corporation a mediados de la década de los cincuenta para utilizarse en aplicaciones científicas y de ingeniería que requerían cálculos matemáticos complejos. En la actualidad, Fortran se utiliza ampliamente en aplicaciones de ingeniería. COBOL (COmmon Business Oriented Language, Lenguaje común orientado a negocios) fue desarrollado a finales de la década de los cincuenta por fabricantes de computadoras, el gobierno estadounidense y usuarios de computadoras de la industria. COBOL se utiliza en aplicaciones comerciales que requieren de una manipulación precisa y eficiente de grandes volúmenes de datos. Gran parte del software de negocios aún se programa en COBOL. Durante la década de los sesenta, muchos de los grandes esfuerzos para el desarrollo de software encontraron severas dificultades. Los itinerarios de software generalmente se retrasaban, los costos rebasaban en gran medida a los presupuestos y los productos terminados no eran confiables. La gente comenzó a darse cuenta de que el desarrollo de software era una actividad mucho más compleja de lo que habían imaginado. Las actividades de

www.elsolucionario.net

10

Capítulo 1

Introducción a las computadoras, Internet y Web

investigación en la década de los sesenta dieron como resultado la evolución de la programación estructurada (un método disciplinado para escribir programas que fueran más claros, fáciles de probar y depurar, y más fáciles de modificar que los programas extensos producidos con técnicas anteriores). Uno de los resultados más tangibles de esta investigación fue el desarrollo del lenguaje de programación Pascal por el profesor Niklaus Wirth, en 1971. Pascal, cuyo nombre se debe al matemático y filósofo Blaise Pascal del siglo diecisiete, se diseñó para la enseñanza de la programación estructurada en ambientes académicos, y de inmediato se convirtió en el lenguaje de programación preferido en la mayoría de las universidades. Pascal carece de muchas de las características necesarias para poder utilizarse en aplicaciones comerciales, industriales y gubernamentales, por lo que no ha sido muy aceptado en estos entornos. El lenguaje de programación Ada se desarrolló bajo el patrocinio del Departamento de Defensa de los Estados Unidos (DOD) durante la década de los setenta y los primeros años de la década de los ochenta. Cientos de lenguajes independientes se utilizaron para producir los sistemas de software masivos de comando y control del departamento de defensa. Éste quería un solo lenguaje que pudiera satisfacer la mayoría de sus necesidades. El nombre del lenguaje es en honor de Lady Ada Lovelace, hija del poeta Lord Byron. A Lady Lovelace se le atribuye el haber escrito el primer programa para computadoras en el mundo, a principios de la década de1800 (para la Máquina Analítica, un dispositivo de cómputo mecánico diseñado por Charles Babbage). Una de las características importantes de Ada se conoce como multitarea, la cual permite a los programadores especificar que muchas actividades ocurran en paralelo. Java, a través de una técnica que se conoce como subprocesamiento múltiple, también permite a los programadores escribir programas con actividades paralelas.

1.12 BASIC, Visual Basic, Visual C++, C# y .NET El lenguaje de programación BASIC (Beginner´s All-Purpose Symbolic Instruction Code, Código de instrucciones simbólicas de uso general para principiantes) fue desarrollado a mediados de la década de los sesenta en el Dartmouth College, como un medio para escribir programas simples. El propósito principal de BASIC era que los principiantes se familiarizaran con las técnicas de programación. El lenguaje Visual Basic de Microsoft se introdujo a principios de la década de los noventa para simplificar el desarrollo de aplicaciones para Microsoft Windows, y es uno de los lenguajes de programación más populares en el mundo. Las herramientas de desarrollo más recientes de Microsoft forman parte de su estrategia a nivel corporativo para integrar Internet y Web en las aplicaciones de computadora. Esta estrategia se implementa en la plataforma .NET de Microsoft, la cual proporciona a los desarrolladores las herramientas que necesitan para crear y ejecutar aplicaciones de computadora que puedan ejecutarse en computadoras distribuidas a través de Internet. Los tres principales lenguajes de programación de Microsoft son Visual Basic .NET (basado en el lenguaje BASIC original), Visual C++ .NET (basado en C++) y C# (basado en C++ y Java, y desarrollado expresamente para la plataforma .NET). Los desarrolladores que utilizan .NET pueden escribir componentes de software en el lenguaje con el que estén más familiarizados y formar aplicaciones al combinar esos componentes con los ya escritos en cualquier lenguaje .NET.

1.13 Entorno de desarrollo típico en Java Ahora explicaremos los pasos típicos usados para crear y ejecutar un programa en Java, utilizando un entorno de desarrollo de Java (el cual se ilustra en la figura 1.1). Por lo general, los programas en Java pasan a través de cinco fases: edición, compilación, carga, verificación y ejecución. Hablamos sobre estos conceptos en el contexto del JDK 6.0 de Sun Microsystems, Inc. Puede descargar el JDK más actualizado y su documentación en java.sun.com/javase/6/download.jsp. Siga cuidadosamente las instrucciones de instalación para el JDK que se proporcionan en la sección Antes de empezar (o en java.sun.com/ javase/6/webnotes/install/index.htm) para asegurarse de configurar su computadora apropiadamente para compilar y ejecutar programas en Java. También es conveniente que visite el centro para principiantes en Java (New to Java Center) de Sun en: java.sun.com/developer/onlineTraining/new2java/index.html

[Nota: este sitio Web proporciona las instrucciones de instalación para Windows, Linux y MacOS X. Si usted no utiliza uno de estos sistemas operativos, consulte los manuales del entorno de Java de su sistema, o pregunte a su

www.elsolucionario.net

1.13

Fase 1: edición

Entorno de desarrollo típico en Java

Editor Disco

Fase 2: compilación

Compilador Disco

El programa se crea en un editor y se almacena en disco, en un archivo con la terminación .java El compilador crea los códigos de bytes y los almacena en disco, en un archivo con la terminación .class

Memoria principal Fase 3: carga

Cargador de clases

...

Disco

El cargador de clases lee los archivos .class que contienen códigos de bytes del disco y coloca esos códigos de bytes en la memoria

Memoria principal Fase 4: verificación

Verificador de código de bytes

...

Memoria principal Fase 5: ejecución

Máquina Virtual de Java (JVM)

...

Figura 1.1 | Entorno de desarrollo típico de Java.

www.elsolucionario.net

El verificador de código de bytes confirma que todos los códigos de bytes sean válidos y no violen las restricciones de seguridad de Java

Para ejecutar el programa, la JVM lee los códigos de bytes y los compila “justo a tiempo” (JIT); es decir, los traduce en un lenguaje que la computadora pueda entender. A medida que se ejecuta el programa, existe la posibilidad de que almacene los valores de datos en la memoria principal

11

12

Capítulo 1

Introducción a las computadoras, Internet y Web

instructor cómo puede lograr estas tareas con base en el sistema operativo de su computadora. Además, tenga en cuenta que en ocasiones los vínculos Web se rompen a medida que las compañías evolucionan sus sitios Web. Si encuentra un problema con este vínculo o con cualquier otro al que se haga referencia en este libro, visite www. deitel.com para consultar la fe de erratas y notifíquenos su problema al correo electrónico deitel@deitel. com. Le responderemos a la brevedad].

Fase 1: Creación de un programa La fase 1 consiste en editar un archivo con un programa de edición (conocido comúnmente como editor). Usted escribe un programa en Java (conocido, por lo general, como código fuente) utilizando el editor, realiza las correcciones necesarias y guarda el programa en un dispositivo de almacenamiento secundario, como su disco duro. Un nombre de archivo que termina con la extensión .java indica que éste contiene código fuente en Java. En este libro asumimos que usted ya sabe cómo editar un archivo. Dos de los editores que se utilizan ampliamente en sistemas Linux son vi y emacs. En Windows, basta con usar un programa editor simple, como el Bloc de notas. También hay muchos editores de freeware y shareware disponibles para descargarlos de Internet, en sitios como www.download.com. Para las organizaciones que desarrollan sistemas de información extensos, hay entornos de desarrollo integrados (IDEs) disponibles de la mayoría de los proveedores de software, incluyendo Sun Microsystems. Los IDEs proporcionan herramientas que dan soporte al proceso de desarrollo del software, incluyendo editores para escribir y editar programas, y depuradores para localizar errores lógicos. Los IDEs populares son: Eclipse (www.eclipse.org), NetBeans (www.netbeans.org), JBuilder (www. borland.com), JCreator (www.jcreator.com), BlueJ (www.blueJ.org), jGRASP (www.jgrasp.org) y JEdit (www.jedit.org). Java Studio Enterprise de Sun Microsystems (developers.sun.com/prodtech/javatools/ jsenterprise/index.jsp) es una versión mejorada de NetBeans. [Nota: la mayoría de nuestros programas de ejemplo deben operar de manera apropiada con cualquier entorno de desarrollo integrado de Java que cuente con soporte para el JDK 6].

Fase 2: Compilación de un programa en Java para convertirlo en códigos de bytes En la fase 2, el programador utiliza el comando javac (el compilador de Java) para compilar un programa. Por ejemplo, para compilar un programa llamado Bienvenido.java, escriba javac Bienvenido.java

en la ventana de comandos de su sistema (es decir, el indicador de MS-DOS en Windows 95/98/ME, el Símbolo del sistema en Windows NT/2000/XP, el indicador de shell en Linux o la aplicación Terminal en Mac OS X). Si el programa se compila, el compilador produce un archivo .class llamado Bienvenido.class, que contiene la versión compilada del programa. El compilador de Java traduce el código fuente en códigos de bytes que representan las tareas a ejecutar en la fase de ejecución (fase 5). La Máquina Virtual de Java (JVM), una parte del JDK y la base de la plataforma Java, ejecuta los códigos de bytes. Una máquina virtual (VM) es una aplicación de software que simula a una computadora, pero oculta el sistema operativo y el hardware subyacentes de los programas que interactúan con la VM. Si se implementa la misma VM en muchas plataformas computacionales, las aplicaciones que ejecute se podrán utilizar en todas esas plataformas. La JVM es una de las máquinas virtuales más utilizadas. A diferencia del lenguaje máquina, que depende del hardware de una computadora específica, los códigos de bytes son instrucciones independientes de la plataforma; no dependen de una plataforma de hardware en especial. Entonces, los códigos de bytes de Java son portables (es decir, se pueden ejecutar en cualquier plataforma que contenga una JVM que comprenda la versión de Java en la que se compilaron). La JVM se invoca mediante el comando java. Por ejemplo, para ejecutar una aplicación llamada Bienvenido, debe escribir el comando java Bienvenido

en una ventana de comandos para invocar la JVM, que a su vez inicia los pasos necesarios para ejecutar la aplicación. Esto comienza la fase 3.

Fase 3: Cargar un programa en memoria En la fase 3, el programa debe colocarse en memoria antes de ejecutarse; a esto se le conoce como cargar. El cargador de clases toma los archivos .class que contienen los códigos de bytes del programa y los transfiere a

www.elsolucionario.net

1.14

Generalidades acerca de Java y este libro

13

la memoria principal. El cargador de clases también carga cualquiera de los archivos .class que su programa utilice, y que sean proporcionados por Java. Puede cargar los archivos .class desde un disco en su sistema o a través de una red (como la de su universidad local o la red de la empresa, o incluso desde Internet).

Fase 4: Verificación del código de bytes En la fase 4, a medida que se cargan las clases, el verificador de códigos de bytes examina sus códigos de bytes para asegurar que sean válidos y que no violen las restricciones de seguridad. Java implementa una estrecha seguridad para asegurar que los programas que llegan a través de la red no dañen sus archivos o su sistema (como podrían hacerlo los virus de computadora y los gusanos).

Fase 5: Ejecución En la fase 5, la JVM ejecuta los códigos de bytes del programa, realizando así las acciones especificadas por el mismo. En las primeras versiones de Java, la JVM era tan sólo un intérprete de códigos de bytes de Java. Esto hacía que la mayoría de los programas se ejecutaran con lentitud, ya que la JVM tenía que interpretar y ejecutar un código de bytes a la vez. Por lo general, las JVMs actuales ejecutan códigos de bytes usando una combinación de la interpretación y la denominada compilación justo a tiempo (JIT). En este proceso, la JVM analiza los códigos de bytes a medida que se interpretan, buscando puntos activos: partes de los códigos de bytes que se ejecutan con frecuencia. Para estas partes, un compilador justo a tiempo (JIT) (conocido como compilador HotSpot de Java) traduce los códigos de bytes al lenguaje máquina correspondiente a la computadora. Cuando la JVM encuentra estas partes compiladas nuevamente, se ejecuta el código en lenguaje máquina, que es más rápido. Por ende, los programas en Java en realidad pasan por dos fases de compilación: una en la cual el código fuente se traduce a código de bytes (para tener portabilidad a través de las JVMs en distintas plataformas computacionales) y otra en la que, durante la ejecución, los códigos de bytes se traducen en lenguaje máquina para la computadora actual en la que se ejecuta el programa.

Problemas que pueden ocurrir en tiempo de ejecución Es probable que los programas no funcionen la primera vez. Cada una de las fases anteriores puede fallar, debido a diversos errores que describiremos en este texto. Por ejemplo, un programa en ejecución podría intentar una división entre cero (una operación ilegal para la aritmética con números enteros en Java). Esto haría que el programa de Java imprimiera un mensaje de error. Si esto ocurre, tendría que regresar a la fase de edición, hacer las correcciones necesarias y proseguir con las fases restantes nuevamente, para determinar que las correcciones resolvieron el(los) problema(s). [Nota: la mayoría de los programas en Java reciben o producen datos. Cuando decimos que un programa muestra un mensaje, por lo general, queremos decir que muestra ese mensaje en la pantalla de su computadora. Los mensajes y otros datos pueden enviarse a otros dispositivos, como los discos y las impresoras, o incluso a una red para transmitirlos a otras computadoras].

Error común de programación 1.1 Los errores, como la división entre cero, ocurren a medida que se ejecuta un programa, de manera que a estos errores se les llama errores en tiempo de ejecución. Los errores fatales en tiempo de ejecución hacen que los programas terminen de inmediato, sin haber realizado correctamente su trabajo. Los errores no fatales en tiempo de ejecución permiten a los programas ejecutarse hasta terminar su trabajo, lo que a menudo produce resultados incorrectos.

1.14 Generalidades acerca de Java y este libro Java es un poderoso lenguaje de programación. En ocasiones, los programadores experimentados se enorgullecen en poder crear un uso excéntrico, deformado e intrincado de un lenguaje. Ésta es una mala práctica de programación; ya que hace que: los programas sean más difíciles de leer, se comporten en forma extraña, sean más difíciles de probar y depurar, y más difíciles de adaptarse a los cambiantes requerimientos. Este libro está enfocado en la claridad. A continuación se muestra nuestro primer tip de Buena práctica de programación:

Buena práctica de programación 1.1 Escriba sus programas de Java en una manera simple y directa. A esto se le conoce algunas veces como KIS (Keep It Simple, simplifíquelo). No “extiendas” el lenguaje experimentando con usos excéntricos.

www.elsolucionario.net

14

Capítulo 1

Introducción a las computadoras, Internet y Web

Seguramente habrá escuchado que Java es un lenguaje portable y que los programas escritos en él pueden ejecutarse en diversas computadoras. En general, la portabilidad es una meta elusiva.

Tip de portabilidad 1.2 Aunque es más fácil escribir programas portables en Java que en la mayoría de los demás lenguajes de programación, las diferencias entre compiladores, JVMs y computadoras pueden hacer que la portabilidad sea difícil de lograr. No basta con escribir programas en Java para garantizar su portabilidad.

Tip para prevenir errores 1.1 Para asegurarse de que sus programas de Java trabajen correctamente para las audiencias a las que están destinados, pruébelos siempre en todos los sistemas en los que tenga pensado ejecutarlos.

Comparamos nuestra presentación con la documentación de Java de Sun, para verificar que sea completa y precisa. Sin embargo, Java es un lenguaje extenso, y ningún libro puede cubrir todos los temas. En la página java.sun.com/javase/6/docs/api/index.html existe una versión de la documentación de las APIs de Java; también puede descargar esta documentación en su propia computadora, visitando java.sun.com/javase/6/ download.jsp. Para obtener detalles adicionales sobre muchos aspectos del desarrollo en Java, visite java.sun. com/reference/docs/index.html.

Buena práctica de programación 1.2 Lea la documentación para la versión de Java que esté utilizando. Consulte esta documentación con frecuencia, para asegurarse de conocer la vasta colección de herramientas disponibles en Java, y para asegurarse de que las está utilizando correctamente.

Buena práctica de programación 1.3 Su computadora y su compilador son buenos maestros. Si, después de leer cuidadosamente el manual de documentación de Java, todavía no está seguro de cómo funciona alguna de sus características, experimente y vea lo que ocurre. Analice cada error o mensaje de advertencia que obtenga al compilar sus programas (a éstos se les llama errores en tiempo de compilación o errores de compilación), y corrija los programas para eliminar estos mensajes.

Observación de ingeniería de software 1.4 Algunos programadores gustan de leer el código fuente para las clases de la API de Java, para determinar cómo funcionan las clases y aprender técnicas de programación adicionales.

1.15 Prueba de una aplicación en Java En esta sección, ejecutará su primera aplicación en Java e interactuará con ella. Para empezar, ejecutará una aplicación de ATM, la cual simula las transacciones que se llevan a cabo al utilizar una máquina de cajero automático, o ATM (por ejemplo, retirar dinero, realizar depósitos y verificar los saldos de las cuentas). Aprenderá a crear esta aplicación en el ejemplo práctico opcional orientado a objetos que se incluye en los capítulos 1-8 y 10. La figura 1.10 al final de esta sección sugiere otras aplicaciones interesantes que también puede probar después de terminar con la prueba del ATM. Para los fines de esta sección supondremos que está utilizando Microsoft Windows. En los siguientes pasos, ejecutará la aplicación y realizará diversas transacciones. Los elementos y la funcionalidad que podemos ver en esta aplicación son típicos de lo que aprenderá a programar en este libro. [Nota: utilizamos diversos tipos de letra para diferenciar las características que se ven en una pantalla (por ejemplo, el Símbolo del sistema) y los elementos que no se relacionan directamente con una pantalla. Nuestra convención es enfatizar las características de la pantalla como los títulos y menús (por ejemplo, el menú Archivo) en una fuente Helvetica sans-serif en negritas, y enfatizar los elementos que no son de la pantalla, como los nombres de archivo o los datos de entrada (como NombrePrograma.java) en una fuente Lucida sans-serif. Como tal vez ya se haya dado cuenta, cuando se ofrece la definición de algún término ésta aparece en negritas. En las figuras en esta sección, resaltamos en gris la entrada del usuario requerida por cada paso, y señalamos las partes importantes de la aplicación con líneas y texto. Para aumentar la visibilidad de estas características, modificamos el color de fondo de las ventanas del Símbolo del sistema].

www.elsolucionario.net

1.15

Prueba de una aplicación en Java

15

1. Revise su configuración. Lea la sección Antes de empezar para verificar si instaló correctamente Java, y observe si copió los ejemplos del libro en su disco duro. 2 Localice la aplicación completa. Abra una ventana Símbolo del sistema. Para ello, puede seleccionar Inicio | Todos los programas | Accesorios | Símbolo del sistema. Cambie al directorio de la aplicación del ATM escribiendo cd C:\ejemplos\ATM, y después oprima Intro (figura 1.2). El comando cd se utiliza para cambiar de directorio. 3. Ejecute la aplicación del ATM. Escriba el comando java EjemploPracticoATM (figura 1.3) oprima Intro. Recuerde que el comando java, seguido del nombre del archivo .class (en este caso, EjemploPracticoATM), ejecuta la aplicación. Si especificamos la extensión .class al usar el comando java se produce un error. [Nota: los comandos en Java son sensibles a mayúsculas/minúsculas. Es importante escribir el nombre de esta aplicación con las letras A, T y M mayúsculas en “ATM”, una letra E mayúscula en “Ejemplo” y una letra P mayúscula en “Practico”. De no ser así, la aplicación no se ejecutará]. Si recibe el mensaje de error "Exception in thread "main" java.lang.NoClassDefFoundError: EjemploPracticoATM", entonces su sistema tiene un problema con CLASSPATH. Consulte la sección Antes de empezar para obtener instrucciones acerca de cómo corregir este problema. 4. Escriba un número de cuenta. Cuando la aplicación se ejecuta por primera vez, muestra el mensaje "Bienvenido!" y le pide un número de cuenta. Escriba 12345 en el indicador "Escriba su numero de cuenta:" (figura 1.4) y oprima Intro.

Use el comando cd para cambiar de directorio

Ubicación de los archivos de la aplicación del ATM

Figura 1.2 | Abrir una ventana Símbolo del sistema en Windows XP y cambiar de directorio.

Figura 1.3 | Uso del comando java para ejecutar la aplicación del ATM.

Mensaje de bienvenida del ATM

Indicador que pide el número de cuenta

Figura 1.4 | La aplicación pide al usuario un número de cuenta.

www.elsolucionario.net

16

Capítulo 1

Introducción a las computadoras, Internet y Web

5. Escriba un NIP. Una vez que introduzca un número de cuenta válido, la aplicación mostrará el indicador "Escriba su NIP:". Escriba "54321" como su NIP (Número de Identificación Personal) válido y oprima Intro. A continuación aparecerá el menú principal del ATM, que contiene una lista de opciones (figura 1.5). 6. Revise el saldo de la cuenta. Seleccione la opción 1, "Ver mi saldo" del menú del ATM (figura 1.6). A continuación la aplicación mostrará dos números: el Saldo disponible ($1,000.00) y el Saldo total ($1,200.00). El saldo disponible es la cantidad máxima de dinero en su cuenta, disponible para retirarla en un momento dado. En algunos casos, ciertos fondos como los depósitos recientes, no están disponibles de inmediato para que el usuario pueda retirarlos, por lo que el saldo disponible puede ser menor que el saldo total, como en este caso. Después de mostrar la información de los saldos de la cuenta, se muestra nuevamente el menú principal de la aplicación. 7. Retire dinero de la cuenta. Seleccione la opción 2, "Retirar efectivo", del menú de la aplicación. A continuación aparecerá (figura 1.7) una lista de montos en dólares (por ejemplo: 20, 40, 60, 100 y 200). También tendrá la oportunidad de cancelar la transacción y regresar al menú principal. Retire $100 seleccionando la opción 4. La aplicación mostrará el mensaje "Tome su efectivo ahora" y regresará al menú principal. [Nota: por desgracia, esta aplicación sólo simula el comportamiento de un verdadero ATM, por lo cual no dispensa efectivo en realidad].

Escriba un NIP válido

Menú principal del ATM

Figura 1.5 | El usuario escribe un número NIP válido y aparece el menú principal de la aplicación del ATM.

Información del saldo de la cuenta

Figura 1.6 | La aplicación del ATM muestra la información del saldo de la cuenta del usuario.

www.elsolucionario.net

1.15

Prueba de una aplicación en Java

17

8. Confirme que la información de la cuenta se haya actualizado. En el menú principal, seleccione la opción 1 nuevamente para ver el saldo actual de su cuenta (figura 1.8). Observe que tanto el saldo disponible como el saldo total se han actualizado para reflejar su transacción de retiro. 9. Finalice la transacción. Para finalizar su sesión actual en el ATM, seleccione, del menú principal, la opción 4, "Salir" (figura 1.9). El ATM saldrá del sistema y mostrará un mensaje de despedida al usuario. A continuación, la aplicación regresará a su indicador original, pidiendo el número de cuenta del siguiente usuario. 10. Salga de la aplicación del ATM y cierre la ventana Símbolo del sistema. La mayoría de las aplicaciones cuentan con una opción para salir y regresar al directorio del Símbolo del sistema desde el cual se ejecutó la aplicación. Un ATM real no proporciona al usuario la opción de apagar la máquina ATM. En vez de ello, cuando el usuario ha completado todas las transacciones deseadas y elige la opción del menú para salir, el ATM se reinicia a sí mismo y muestra un indicador para el número de cuenta del siguiente

Menú de retiro del ATM

Figura 1.7 | Se retira el dinero de la cuenta y la aplicación regresa al menú principal.

Confirmación de la información del saldo de la cuenta actualizado después de la transacción de retiro

Figura 1.8 | Verificación del nuevo saldo.

www.elsolucionario.net

18

Capítulo 1

Introducción a las computadoras, Internet y Web

usuario. Como se muestra en la figura 1.9, la aplicación del ATM se comporta de manera similar. Al elegir la opción del menú para salir sólo se termina la sesión del usuario actual con el ATM, no toda la aplicación completa. Para salir realmente de la aplicación del ATM, haga clic en el botón Cerrar (x) en la esquina superior derecha de la ventana Símbolo del sistema. Al cerrar la ventana, la aplicación termina su ejecución.

Aplicaciones adicionales incluidas en Cómo programar en Java, 7ª edición La figura 1.10 lista unas cuantas de los cientos de aplicaciones que se incluyen en los ejemplos y ejercicios del libro. Estos programas presentan muchas de las poderosas y divertidas características de Java. Ejecute estos programas para que conozca más acerca de las aplicaciones que aprenderá a construir en este libro de texto. La carpeta de ejemplos para este capítulo contiene todos los archivos requeridos para ejecutar cada aplicación. Sólo escriba los comandos que se listan en la figura 1.10 en una ventana de Símbolo del sistema.

Mensaje de despedida del ATM

Indicador del número de cuenta para el siguiente usuario

Figura 1.9 | Finalización de una sesión de transacciones con el ATM.

Nombre de la aplicación

Capítulo(s) en donde se ubica

Comandos a ejecutar

Tic-Tac-Toe

Capítulos 8 y 24

cd C:\ejemplos\cap01\Tic-Tac-Toe Java PruebaTicTacToe

Juego de adivinanza

Capítulo 11

cd C:\ejemplos\cap01\JuegoAdivinanza Java JuegoAdivinanza

Animador de logotipos

Capítulo 21

cd C:\ejemplos\cap01\AnimadorLogotipos Java AnimadorLogotipos

Pelota rebotadora

Capítulo 23

cd C:\ejemplos\cap01\PelotaRebotadora Java PelotaRebotadora

Figura 1.10 | Ejemplos de aplicaciones de Java adicionales, incluidas en Cómo programar en Java, 7ª edición.

www.elsolucionario.net

1.16

Ejemplo práctico de Ingeniería de Software: introducción a la tecnología de objetos y UML

19

1.16 Ejemplo práctico de Ingeniería de Software: introducción a la tecnología de objetos y UML Ahora empezaremos nuestra primera introducción al tema de la orientación a objetos, una manera natural de pensar acerca del mundo real y de escribir programas de cómputo. Los capítulos 1-8 y 10 terminan con una sección breve titulada Ejemplo práctico de Ingeniería de Software, en la cual presentamos una introducción cuidadosamente guiada al tema de la orientación a objetos. Nuestro objetivo aquí es ayudarle a desarrollar una forma de pensar orientada a objetos, y de presentarle el Lenguaje Unificado de Modelado™ (UML™), un lenguaje gráfico que permite a las personas que diseñan sistemas de software utilizar una notación estándar en la industria para representarlos. En esta única sección requerida (1.16), presentamos los conceptos y la terminología de la orientación a objetos. Las secciones opcionales en los capítulos 2-8 y 10 presentan el diseño y la implementación orientados a objetos de un software para una máquina de cajero automático (ATM) simple. Las secciones tituladas Ejemplo práctico de Ingeniería de Software al final de los capítulos 2 al 7: • • • • •

analizan un documento de requerimientos típico que describe un sistema de software (el ATM) que construirá determinan los objetos requeridos para implementar ese sistema establecen los atributos que deben tener estos objetos fijan los comportamientos que exhibirán estos objetos especifican la forma en que los objetos deben interactuar entre sí para cumplir con los requerimientos del sistema

Las secciones tituladas Ejemplo práctico de Ingeniería de Software al final de los capítulos 8 y 10 modifican y mejoran el diseño presentado en los capítulos 2 al 7. El apéndice M contiene una implementación completa y funcional en Java del sistema ATM orientado a objetos. Usted experimentará una concisa pero sólida introducción al diseño orientado a objetos con UML. Además, afinará sus habilidades para leer código al ver paso a paso la implementación del ATM en Java, cuidadosamente escrita y bien documentada, en el apéndice M.

Conceptos básicos de la tecnología de objetos Comenzaremos nuestra introducción al tema de la orientación a objetos con cierta terminología clave. En cualquier parte del mundo real puede ver objetos: gente, animales, plantas, automóviles, aviones, edificios, computadoras, etcétera. Los humanos pensamos en términos de objetos. Los teléfonos, casas, semáforos, hornos de microondas y enfriadores de agua son sólo unos cuantos objetos más. Los programas de cómputo, como los programas de Java que leerá en este libro y los que usted mismo escriba, están compuestos por muchos objetos de software con capacidad de interacción. En ocasiones dividimos a los objetos en dos categorías: animados e inanimados. Los objetos animados están “vivos” en cierto sentido; se mueven a su alrededor y hacen cosas. Por otro lado, los objetos inanimados no se mueven por su propia cuenta. Sin embargo, los objetos de ambos tipos tienen ciertas cosas en común. Todos ellos tienen atributos (como tamaño, forma, color y peso), y todos exhiben comportamientos (por ejemplo, una pelota rueda, rebota, se infla y desinfla; un bebé llora, duerme, gatea, camina y parpadea; un automóvil acelera, frena y da vuelta; una toalla absorbe agua). Estudiaremos los tipos de atributos y comportamientos que tienen los objetos de software. Los humanos aprenden acerca de los objetos existentes estudiando sus atributos y observando sus comportamientos. Distintos objetos pueden tener atributos similares y pueden exhibir comportamientos similares. Por ejemplo, pueden hacerse comparaciones entre los bebés y los adultos, y entre los humanos y los chimpancés. El diseño orientado a objetos (DOO) modela el software en términos similares a los que utilizan las personas para describir objetos del mundo real. Este diseño aprovecha las relaciones entre las clases, en donde los objetos de cierta clase (como una clase de vehículos) tienen las mismas características; los automóviles, camiones, pequeños vagones rojos y patines tienen mucho en común. El DOO también aprovecha las relaciones de herencia, en donde las nuevas clases de objetos se derivan absorbiendo las características de las clases existentes y agregando sus propias características únicas. Un objeto de la clase “convertible” ciertamente tiene las características de la clase más general “automóvil” pero, de manera más específica, el techo de un convertible puede ponerse y quitarse.

www.elsolucionario.net

20

Capítulo 1

Introducción a las computadoras, Internet y Web

El diseño orientado a objetos proporciona una manera natural e intuitiva de ver el proceso de diseño de software: a saber, modelando los objetos por sus atributos y comportamientos, de igual forma que como describimos los objetos del mundo real. El DOO también modela la comunicación entre los objetos. Así como las personas se envían mensajes unas a otras (por ejemplo, un sargento ordenando a un soldado que permanezca firme), los objetos también se comunican mediante mensajes. Un objeto cuenta de banco puede recibir un mensaje para reducir su saldo por cierta cantidad, debido a que el cliente ha retirado esa cantidad de dinero. El DOO encapsula (es decir, envuelve) los atributos y las operaciones (comportamientos) en los objetos; los atributos y las operaciones de un objeto se enlazan íntimamente entre sí. Los objetos tienen la propiedad de ocultamiento de información. Esto significa que los objetos pueden saber cómo comunicarse entre sí a través de interfaces bien definidas, pero por lo general no se les permite saber cómo se implementan otros objetos; los detalles de la implementación se ocultan dentro de los mismos objetos. Por ejemplo, podemos conducir un automóvil con efectividad, sin necesidad de saber los detalles acerca de cómo funcionan internamente los motores, las transmisiones y los sistemas de escape; siempre y cuando sepamos cómo usar el pedal del acelerador, el pedal del freno, el volante, etcétera. Más adelante veremos por qué el ocultamiento de información es tan imprescindible para la buena ingeniería de software. Los lenguajes como Java son orientados a objetos. La programación en dichos lenguajes se llama programación orientada a objetos (POO), y permite a los programadores de computadoras implementar un diseño orientado a objetos como un sistema funcional. Por otra parte, los lenguajes como C son por procedimientos, de manera que la programación tiende a ser orientada a la acción. En C, la unidad de programación es la función. Los grupos de acciones que realizan cierta tarea común se forman en funciones, y las funciones se agrupan para formar programas. En Java, la unidad de programación es la clase a partir de la cual se instancian (crean) los objetos en un momento dado. Las clases en Java contienen métodos (que implementan operaciones y son similares a las funciones en C) y campos (que implementan atributos). Los programadores de Java se concentran en crear clases. Cada clase contiene campos, además del conjunto de métodos que manipulan esos campos y proporcionan servicios a clientes (es decir, otras clases que utilizan esa clase). El programador utiliza las clases existentes como bloques de construcción para crear nuevas clases. Las clases son para los objetos lo que los planos de construcción, para las casas. Así como podemos construir muchas casas a partir de un plano, podemos instanciar (crear) muchos objetos a partir de una clase. No puede cocinar alimentos en la cocina de un plano de construcción; puede cocinarlos en la cocina de una casa. Las clases pueden tener relaciones con otras clases. Por ejemplo, en un diseño orientado a objetos de un banco, la clase “cajero” necesita relacionarse con las clases “cliente”, “cajón de efectivo”, “bóveda”, etcétera. A estas relaciones se les llama asociaciones. Al empaquetar el software en forma de clases, los sistemas de software posteriores pueden reutilizar esas clases. Los grupos de clases relacionadas se empaquetan comúnmente como componentes reutilizables. Así como los corredores de bienes raíces dicen a menudo que los tres factores más importantes que afectan el precio de los bienes raíces son “la ubicación, la ubicación y la ubicación”, las personas en la comunidad de software dicen a menudo que los tres factores más importantes que afectan el futuro del desarrollo de software son “la reutilización, la reutilización y la reutilización”. Reutilizar las clases existentes cuando se crean nuevas clases y programas es un proceso que ahorra tiempo y esfuerzo; también ayuda a los programadores a crear sistemas más confiables y efectivos, ya que las clases y componentes existentes a menudo han pasado por un proceso extenso de prueba, depuración y optimización del rendimiento. Evidentemente, con la tecnología de objetos podemos crear la mayoría del software que necesitaremos mediante la combinación de clases, así como los fabricantes de automóviles combinan las piezas intercambiables. Cada nueva clase que usted cree tendrá el potencial de convertirse en una valiosa pieza de software, que usted y otros programadores podrán usar para agilizar y mejorar la calidad de los futuros esfuerzos de desarrollo de software.

Introducción al análisis y diseño orientados a objetos (A/DOO) Pronto estará escribiendo programas en Java. ¿Cómo creará el código para sus programas? Tal vez, como muchos programadores principiantes, simplemente encenderá su computadora y empezará a teclear. Esta metodología puede funcionar para programas pequeños (como los que presentamos en los primeros capítulos del libro) pero ¿qué haría usted si se le pidiera crear un sistema de software para controlar miles de máquinas de cajero automático para un importante banco? O suponga que le pidieron trabajar en un equipo de 1,000 desarrolladores de software para construir el nuevo sistema de control de tráfico aéreo de Estados Unidos. Para proyectos tan grandes y complejos, no podría simplemente sentarse y empezar a escribir programas.

www.elsolucionario.net

1.16

Ejemplo práctico de Ingeniería de Software: introducción a la tecnología de objetos y UML

21

Para crear las mejores soluciones, debe seguir un proceso detallado para analizar los requerimientos de su proyecto (es decir, determinar qué es lo que se supone debe hacer el sistema) y desarrollar un diseño que cumpla con esos requerimientos (es decir, decidir cómo debe hacerlo el sistema). Idealmente usted pasaría por este proceso y revisaría cuidadosamente el diseño (o haría que otros profesionales de software lo revisaran) antes de escribir cualquier código. Si este proceso implica analizar y diseñar su sistema desde un punto de vista orientado a objetos, lo llamamos un proceso de análisis y diseño orientado a objetos (A/DOO). Los programadores experimentados saben que el análisis y el diseño pueden ahorrar innumerables horas, ya que les ayudan a evitar un método de desarrollo de un sistema mal planeado, que tiene que abandonarse en plena implementación, con la posibilidad de desperdiciar una cantidad considerable de tiempo, dinero y esfuerzo. A/DOO es el término genérico para el proceso de analizar un problema y desarrollar un método para resolverlo. Los pequeños problemas como los que se describen en los primeros capítulos de este libro no requieren de un proceso exhaustivo de A/DOO. Podría ser suficiente con escribir pseudocódigo antes de empezar a escribir el código en Java; el pseudocódigo es un medio informal para expresar la lógica de un programa. En realidad no es un lenguaje de programación, pero podemos usarlo como un tipo de bosquejo para guiarnos mientras escribimos nuestro código. En el capítulo 4 presentamos el pseudocódigo. A medida que los problemas y los grupos de personas que los resuelven aumentan en tamaño, los métodos de A/DOO se vuelven más apropiados que el pseudocódigo. Idealmente, un grupo debería acordar un proceso estrictamente definido para resolver su problema, y establecer también una manera uniforme para que los miembros del grupo se comuniquen los resultados de ese proceso entre sí. Aunque existen diversos procesos de A/DOO, hay un lenguaje gráfico para comunicar los resultados de cualquier proceso A/DOO que se ha vuelto muy popular. Este lenguaje, conocido como Lenguaje Unificado de Modelado (UML), se desarrolló a mediados de la década de los noventa, bajo la dirección inicial de tres metodologistas de software: Grady Booch, James Rumbaugh e Ivar Jacobson.

Historia de UML En la década de los ochenta, un creciente número de empresas comenzó a utilizar la POO para crear sus aplicaciones, lo cual generó la necesidad de un proceso estándar de A/DOO. Muchos metodologistas (incluyendo a Booch, Rumbaugh y Jacobson) produjeron y promocionaron, por su cuenta, procesos separados para satisfacer esta necesidad. Cada uno de estos procesos tenía su propia notación, o “lenguaje” (en forma de diagramas gráficos), para transmitir los resultados del análisis y el diseño. A principios de la década de los noventa, diversas compañías (e inclusive diferentes divisiones dentro de la misma compañía) utilizaban sus propios procesos y notaciones únicos. Al mismo tiempo, estas compañías querían utilizar herramientas de software que tuvieran soporte para sus procesos particulares. Con tantos procesos, se les dificultó a los distribuidores de software proporcionar dichas herramientas. Evidentemente era necesario contar con una notación y procesos estándar. En 1994, James Rumbaugh se unió con Grady Booch en Rational Software Corporation (ahora una división de IBM), y comenzaron a trabajar para unificar sus populares procesos. Pronto se unió a ellos Ivar Jacobson. En 1996, el grupo liberó las primeras versiones de UML para la comunidad de ingeniería de software, solicitando retroalimentación. Casi al mismo tiempo, una organización conocida como Object Management Group™ (OMG™, Grupo de administración de objetos) hizo una invitación para participar en la creación de un lenguaje común de modelado. El OMG (www.omg.org) es una organización sin fines de lucro que promueve la estandarización de las tecnologías orientadas a objetos, emitiendo lineamientos y especificaciones como UML. Varias empresas (entre ellas HP, IBM, Microsoft, Oracle y Rational Software) habían reconocido ya la necesidad de un lenguaje común de modelado. Estas compañías formaron el consorcio UML Partners (Socios de UML) en respuesta a la solicitud de proposiciones por parte del OMG (el consorcio que desarrolló la versión 1.1 de UML y la envió al OMG). La propuesta fue aceptada y, en 1997, el OMG asumió la responsabilidad del mantenimiento y revisión de UML en forma continua. La versión 2 que está ahora disponible marca la primera modificación importante al UML desde el estándar de la versión 1.1 de 1997. A lo largo de este libro, presentaremos la terminología y notación de UML 2.

¿Qué es UML? UML es ahora el esquema de representación gráfica más utilizado para modelar sistemas orientados a objetos. Evidentemente ha unificado los diversos esquemas de notación populares. Aquellos quienes diseñan sistemas utilizan el lenguaje (en forma de diagramas) para modelar sus sistemas.

www.elsolucionario.net

22

Capítulo 1

Introducción a las computadoras, Internet y Web

Una característica atractiva es su flexibilidad. UML es extensible (es decir, capaz de mejorarse con nuevas características) e independiente de cualquier proceso de A/DOO específico. Los modeladores de UML tienen la libertad de diseñar sistemas utilizando varios procesos, pero todos los desarrolladores pueden ahora expresar esos diseños con un conjunto de notaciones gráficas estándar. UML es un lenguaje gráfico complejo, con muchas características. En nuestras secciones del Ejemplo práctico de Ingeniería de Software, presentamos un subconjunto conciso y simplificado de estas características. Luego utilizamos este subconjunto para guiarlo a través de la experiencia de su primer diseño con UML, la cual está dirigida a los programadores principiantes orientados a objetos en cursos de programación de primer o segundo semestre.

Recursos Web de UML Para obtener más información acerca de UML, consulte los siguientes sitios Web: www.uml.org

Esta página de recursos de UML del Grupo de Administración de Objetos (OMG) proporciona documentos de la especificación para UML y otras tecnologías orientadas a objetos. www.ibm.com/software/rational/uml

Ésta es la página de recursos de UML para IBM Rational, sucesor de Rational Software Corporation (la compañía que creó a UML). en.wikipedia.org/wiki/UML

La definición de Wikipedia de UML. Este sitio también ofrece vínculos a muchos recursos adicionales de UML. es.wikipedia.org/wiki/UML

La definición de Wikipedia del UML en español.

Lecturas recomendadas Los siguientes libros proporcionan información acerca del diseño orientado a objetos con UML: Ambler, S. The Object Primer: Agile Model-Driven Development with UML 2.0, Third Edition. Nueva York: Cambridge University Press, 2005. Arlow, J. e I. Neustadt. UML and the Unified Process: Practical Object-Oriented Analysis and Design, Second Edition. Boston: Addison-Wesley Professional, 2006. Fowler, M. UML Distilled, Third Edition: A Brief Guide to the Standard Object Modeling Language. Boston: AddisonWesley Professional, 2004. Rumbaugh, J., I. Jacobson y G. Booch. The Unified Modeling Language User Guide, Second Edition. Boston: AddisonWesley Professional, 2006.

Ejercicios de autorrepaso de la sección 1.16 1.1 Liste tres ejemplos de objetos reales que no mencionamos. Para cada objeto, liste varios atributos y comportamientos. 1.2

El pesudocódigo es __________. a) otro término para el A/DOO b) un lenguaje de programación utilizado para visualizar diagramas de UML c) un medio informal para expresar la lógica de un programa d) un esquema de representación gráfica para modelar sistemas orientados a objetos

1.3

El UML se utiliza principalmente para __________. a) probar sistemas orientados a objetos b) diseñar sistemas orientados a objetos c) implementar sistemas orientados a objetos d) a y b

Respuestas a los ejercicios de autorrepaso de la sección 1.16 1.1 [Nota: las respuestas pueden variar]. a) Los atributos de una televisión incluyen el tamaño de la pantalla, el número de colores que puede mostrar, su canal actual y su volumen actual. Una televisión se enciende y se apaga, cam-

www.elsolucionario.net

1.17

Web 2.0

23

bia de canales, muestra video y reproduce sonidos. b) Los atributos de una cafetera incluyen el volumen máximo de agua que puede contener, el tiempo requerido para preparar una jarra de café y la temperatura del plato calentador bajo la jarra de café. Una cafetera se enciende y se apaga, prepara café y lo calienta. c) Los atributos de una tortuga incluyen su edad, el tamaño de su caparazón y su peso. Una tortuga camina, se mete en su caparazón, emerge del mismo y come vegetación. 1.2

c.

1.3

b.

1.17 Web 2.0 Literalmente, la Web explotó a mediados de la década de los noventa, pero surgieron tiempos difíciles a principios del año 2000, debido al desplome económico de punto com. Al resurgimiento que empezó aproximadamente en el 2004, se le conoce como Web 2.0. La primera Conferencia sobre Web 2.0 se realizó en el 2004. Un año después, el término “Web 2.0” obtuvo aproximadamente 10 millones de coincidencias en el motor de búsqueda Google, para crecer hasta 60 millones al año siguiente. A Google se le considera en muchas partes como la compañía característica de Web 2.0. Algunas otras son Craigslist (listados gratuitos de anuncios clasificados), Flickr (sitio para compartir fotos), del.icio.us (sitios favoritos de carácter social), YouTube (sitio para compartir videos), MySpace y FaceBook (redes sociales), Salesforce (software de negocios que se ofrece como servicio en línea), Second Life (un mundo virtual), Skype (telefonía por Internet) y Wikipedia (una enciclopedia en línea gratuita). En Deitel & Associates, inauguramos nuestra Iniciativa de Negocios por Internet basada en Web 2.0 en el año 2005. Estamos investigando las tecnologías clave de Web 2.0 y las utilizamos para crear negocios en Internet. Compartimos nuestra investigación en forma de Centros de recursos en www.deitel.com/resourcecenters. html. Cada semana anunciamos los Centros de recursos más recientes en nuestro boletín de correo electrónico Deitel® Buzz Online (www.deitel.com/newsletter/subscribe.html). Cada uno de estos centros lista muchos vínculos a contenido y software gratuito en Internet. En este libro incluimos un tratamiento detallado sobre los servicios Web (capítulo 28) y presentamos la nueva metodología de desarrollo de aplicaciones, conocida como mashups (apéndice H), en la que puede desarrollar rápidamente aplicaciones poderosas e intrigantes, al combinar servicios Web complementarios y otras fuentes de información provenientes de dos o más organizaciones. Un mashup popular es www.housingmaps.com, el cual combina los listados de bienes raíces de www.craigslist.org con las capacidades de los mapas de Google Maps para mostrar las ubicaciones de los apartamentos para renta en un área dada. Ajax es una de las tecnologías más importantes de Web 2.0. Aunque el uso del término explotó en el 2005, es sólo un término que nombra a un grupo de tecnologías y técnicas de programación que han estado en uso desde finales de la década de los noventa. Ajax ayuda a las aplicaciones basadas en Internet a funcionar como las aplicaciones de escritorio; una tarea difícil, dado que dichas aplicaciones sufren de retrasos en la transmisión, a medida que los datos se intercambian entre su computadora y las demás computadoras en Internet. Mediante el uso de Ajax, las aplicaciones como Google Maps han logrado un desempeño excelente, además de la apariencia visual de las aplicaciones de escritorio. Aunque no hablaremos sobre la programación “pura” con Ajax en este libro (que es bastante compleja), en el capítulo 27 mostraremos cómo crear aplicaciones habilitadas para Ajax mediante el uso de los componentes de JavaServer Faces (JSF) habilitados para Ajax. Los blogs son sitios Web (actualmente hay como 60 millones de ellos) similares a un diario en línea, en donde las entradas más recientes aparecen primero. Los “bloggers” publican rápidamente sus opiniones acerca de las noticias, lanzamientos de productos, candidatos políticos, temas controversiales, y de casi todo lo demás. A la colección de todos los blogs y de la comunidad de “blogging” se le conoce como blogósfera, y cada vez está teniendo más influencia. Technorati es el líder en motores de búsqueda de blogs. Las fuentes RSS permiten a los sitios enviar información a sus suscriptores. Un uso común de las fuentes RSS es enviar las publicaciones más recientes de los blogs, a las personas que se suscriben a éstos. Los flujos de información RSS en Internet están creciendo de manera exponencial. Web 3.0 es otro nombre para la siguiente generación de la Web, que también se le conoce como Web Semántica. Casi todo el contenido de Web 1.0 estaba basado en HTML. Web 2.0 está utilizando cada vez más el XML, en especial en tecnologías como las fuentes RSS. Web 3.0 utilizará aún más el XML, creando una “Web de significado”. Si usted es un estudiante que busca un excelente artículo de presentación o un tema para una tesis, o si es un emprendedor que busca oportunidades de negocios, dé un vistazo a nuestro Centro de recursos sobre Web 3.0.

www.elsolucionario.net

24

Capítulo 1

Introducción a las computadoras, Internet y Web

Para seguir los últimos desarrollos en Web 2.0, visite www.techcrunch.com y www.slashdot.org, y revise la lista creciente de Centros de recursos relacionados con Web 2.0 en www.deitel.com/resourcecenters.html.

1.18 Tecnologías de software En esta sección hablaremos sobre varias “palabras de moda” que escuchará en la comunidad de desarrollo de software. Creamos Centros de recursos sobre la mayoría de estos temas, y hay muchos por venir. Agile Software Development (Desarrollo Ágil de Software) es un conjunto de metodologías que tratan de implementar software rápidamente, con menos recursos que las metodologías anteriores. Visite los sitios de Agile Alliance (www.agilealliance.org) y Agile Manifesto (www.agilemanifesto.org). También puede visitar el sitio en español www.agile-spain.com. Extreme programming (XP) (Programación extrema (PX)) es una de las diversas tecnologías de desarrollo ágil. Trata de desarrollar software con rapidez. El software se libera con frecuencia en pequeños incrementos, para alentar la rápida retroalimentación de los usuarios. PX reconoce que los requerimientos de los usuarios cambian a menudo, y que el software debe cumplir con esos requerimientos rápidamente. Los programadores trabajan en pares en una máquina, de manera que la revisión del código se realiza de inmediato, a medida que se crea el código. Todos en el equipo deben poder trabajar con cualquier parte del código. Refactoring (Refabricación) implica la reformulación del código para hacerlo más claro y fácil de mantener, al tiempo que se preserva su funcionalidad. Se emplea ampliamente con las metodologías de desarrollo ágil. Hay muchas herramientas de refabricación disponibles para realizar las porciones principales de la reformulación de manera automática. Los patrones de diseño son arquitecturas probadas para construir software orientado a objetos flexible y que pueda mantenerse (vea el apéndice P Web adicional). El campo de los patrones de diseño trata de enumerar a los patrones recurrentes, y de alentar a los diseñadores de software para que los reutilicen y puedan desarrollar un software de mejor calidad con menos tiempo, dinero y esfuerzo. Programación de juegos. El negocio de los juegos de computadora es más grande que el negocio de las películas de estreno. Ahora hay cursos universitarios, e incluso maestrías, dedicados a las técnicas sofisticadas de software que se utilizan en la programación de juegos. Vea nuestros Centros de recursos sobre Programación de juegos y Proyectos de programación. El software de código fuente abierto es un estilo de desarrollo de software que contrasta con el desarrollo propietario, que dominó los primeros años del software. Con el desarrollo de código fuente abierto, individuos y compañías contribuyen sus esfuerzos en el desarrollo, mantenimiento y evolución del software, a cambio del derecho de usar ese software para sus propios fines, comúnmente sin costo. Por lo general, el código fuente abierto se examina a detalle por una audiencia más grande que el software propietario, por lo cual los errores se eliminan con más rapidez. El código fuente abierto también promueve más innovación. Sun anunció recientemente que piensa abrir el código fuente de Java. Algunas de las organizaciones de las que se habla mucho en la comunidad de código fuente abierto son Eclipse Foundation (el IDE de Eclipse es popular para el desarrollo de software en Java), Mozilla Foundation (creadores del explorador Web Firefox), Apache Software Foundation (creadores del servidor Web Apache) y SourceForge (que proporciona las herramientas para administrar proyectos de código fuente abierto y en la actualidad cuenta con más de 100,000 proyectos en desarrollo). Linux es un sistema operativo de código fuente abierto, y uno de los más grandes éxitos de la iniciativa de código fuente abierto. MySQL es un sistema de administración de bases de datos con código fuente abierto. PHP es el lenguaje de secuencias de comandos del lado servidor para Internet de código fuente abierto más popular, para el desarrollo de aplicaciones basadas en Internet. LAMP es un acrónimo para el conjunto de tecnologías de código fuente abierto que utilizaron muchos desarrolladores para crear aplicaciones Web: representa a Linux, Apache, MySQL y PHP (o Perl, o Python; otros dos lenguajes que se utilizan para propósitos similares). Ruby on Rails combina el lenguaje de secuencias de comandos Ruby con el marco de trabajo para aplicaciones Web Rails, desarrollado por la compañía 37Signals. Su libro, Getting Real, es una lectura obligatoria para los desarrolladores de aplicaciones Web de la actualidad; puede leerlo sin costo en gettingreal.37signals.com/ toc.php. Muchos desarrolladores de Ruby on Rails han reportado un considerable aumento en la productividad, en comparación con otros lenguajes al desarrollar aplicaciones Web con uso intensivo de bases de datos. Por lo general, el software siempre se ha visto como un producto; la mayoría del software aún se ofrece de esta manera. Si desea ejecutar una aplicación, compra un paquete de software de un distribuidor. Después instala ese software en su computadora y lo ejecuta según sea necesario. Al aparecer nuevas versiones del software, usted lo

www.elsolucionario.net

1.20 Recursos Web

25

actualiza, a menudo con un costo considerable. Este proceso puede volverse incómodo para empresas con decenas de miles de sistemas que deben mantenerse en una extensa colección de equipo de cómputo. Con Software as a Service (SAAS), el software se ejecuta en servidores ubicados en cualquier parte de Internet. Cuando se actualiza ese servidor, todos los clientes a nivel mundial ven las nuevas características; no se necesita instalación local. Usted accede al servidor a través de un explorador Web; éstos son bastante portables, por lo que puede ejecutar las mismas aplicaciones en distintos tipos de computadoras, desde cualquier parte del mundo. Salesforce.com, Google, Microsoft Office Live y Windows Live ofrecen SAAS.

1.19 Conclusión Este capítulo presentó los conceptos básicos de hardware y software, y los conceptos de la tecnología básica de objetos, incluyendo clases, objetos, atributos, comportamientos, encapsulamiento, herencia y polimorfismo. Hablamos sobre los distintos tipos de lenguajes de programación y cuáles son los más utilizados. Conoció los pasos para crear y ejecutar una aplicación de Java mediante el uso del JDK 6 de Sun. El capítulo exploró la historia de Internet y World Wide Web, y la función de Java en cuanto al desarrollo de aplicaciones cliente/servidor distribuidas para Internet y Web. También aprendió acerca de la historia y el propósito de UML: el lenguaje gráfico estándar en la industria para modelar sistemas de software. Por último, realizó pruebas de una o más aplicaciones de Java, similares a los tipos de aplicaciones que aprenderá a programar en este libro. En el capítulo 2 creará sus primeras aplicaciones en Java. Verá ejemplos que muestran cómo los programas imprimen mensajes en pantalla y obtienen información del usuario para procesarla. Analizaremos y explicaremos cada ejemplo, para facilitarle el proceso de aprender a programar en Java.

1.20 Recursos Web Esta sección proporciona muchos recursos que le serán de utilidad a medida que aprenda Java. Los sitios incluyen recursos de Java, herramientas de desarrollo de Java para estudiantes y profesionales, y nuestros propios sitios Web, en donde podrá encontrar descargas y recursos asociados con este libro. También le proporcionaremos un vínculo, en donde podrá suscribirse a nuestro boletín de correo electrónico Deitel® Buzz Online sin costo.

Sitios Web de Deitel & Associates www.deitel.com

Contiene actualizaciones, correcciones y recursos adicionales para todas las publicaciones Deitel. www.deitel.com/newsletter/subscribe.html

Suscríbase al boletín de correo electrónico gratuito Deitel® Buzz Online, para seguir el programa de publicaciones de Deitel & Associates, incluyendo actualizaciones y fe de erratas para este libro. www.prenhall.com/deitel

La página de inicio de Prentice Hall para las publicaciones Deitel. Aquí encontrará información detallada sobre los productos, capítulos de ejemplo y Sitios Web complementarios con recursos para estudiantes e instructores. www.deitel.com/books/jhtp7/

La página de inicio de Deitel & Associates para Cómo programar en Java, 7a edición. Aquí encontrará vínculos a los ejemplos del libro (que también se incluyen en el CD que viene con el libro) y otros recursos.

Centros de recursos de Deitel sobre Java www.deitel.com/Java/

Nuestro Centro de recursos sobre Java se enfoca en la enorme cantidad de contenido gratuito sobre Java, disponible en línea. Empiece aquí su búsqueda de recursos, descargas, tutoriales, documentación, libros, libros electrónicos, diarios, artículos, blogs y más, que le ayudarán a crear aplicaciones en Java. www.deitel.com/JavaSE6Mustang/

Nuestro Centro de recursos sobre Java SE 6 (Mustang) es su guía para la última versión de Java. Este sitio incluye los mejores recursos que encontramos en línea, para ayudarle a empezar con el desarrollo en Java SE 6. www.deitel.com/JavaEE5/

Nuestro Centro de recursos sobre Java Enterprise Edition 5 (Java EE 5). www.deitel.com/JavaCertification/

Nuestro Centro de recursos de evaluación de certificación y valoración.

www.elsolucionario.net

26

Capítulo 1

Introducción a las computadoras, Internet y Web

www.deitel.com/JavaDesignPatterns/

Nuestro Centro de recursos sobre los patrones de diseño de Java. En su libro, Design Patterns: Elements of Reusable Object-Oriented Software (Boston: Addison-Wesley Professional, 1995), la “Banda de los cuatro” (E. Gamma, R. Helm, R. Jonson y J. Vlissides) describen 23 patrones de diseño que proporcionan arquitecturas demostradas para construir sistemas de software orientados a objetos. En este centro de recursos, encontrará discusiones sobre muchos de éstos y otros patrones de diseño. www.deitel.com/CodeSearchEngines/

Nuestro Centro de recursos sobre Motores de Búsqueda de Código y Sitios de Código incluye recursos que los desarrolladores utilizan para buscar código fuente en línea. www.deitel.com/ProgrammingProjects/

Nuestro Centro de recursos sobre Proyectos de Programación es su guía para proyectos de programación estudiantiles en línea.

Sitios Web de Sun Microsystems java.sun.com/developer/onlineTraining/new2java/index.html

El centro “New to Java Center” (Centro para principiantes en Java) en el sitio Web de Sun Microsystems ofrece recursos de capacitación en línea para ayudarle a empezar con la programación en Java. java.sun.com/javase/6/download.jsp

La página de descarga para el Kit de Desarrollo de Java 6 (JDK 6) y su documentación. El JDK incluye todo lo necesario para compilar y ejecutar sus aplicaciones en Java SE 6 (Mustang). java.sun.com/javase/6/webnotes/install/index.html

Instrucciones para instalar el JDK 6 en plataformas Solaris, Windows y Linux. java.sun.com/javase/6/docs/api/index.html

El sitio en línea para la documentación de la API de Java SE 6. java.sun.com/javase

La página de inicio para la plataforma Java Standard Edition. java.sun.com

La página de inicio de la tecnología Java de Sun ofrece descargas, referencias, foros, tutoriales en línea y mucho más. java.sun.com/reference/docs/index.html

El sitio de documentación de Sun para todas las tecnologías de Java. developers.sun.com

La página de inicio de Sun para los desarrolladores de Java proporciona descargas, APIs, ejemplos de código, artículos con asesoría técnica y otros recursos sobre las mejores prácticas de desarrollo en Java.

Editores y Entornos de Desarrollo Integrados www.eclipse.org

El entorno de desarrollo Eclipse puede usarse para desarrollar código en cualquier lenguaje de programación. Puede descargar el entorno y varios complementos (plug-ins) de Java para desarrollar sus programas en Java. www.netbeans.org

El IDE NetBeans. Una de las herramientas de desarrollo para Java más populares, de distribución gratuita. borland.com/products/downloads/download_jbuilder.html

Borland ofrece una versión Foundation Edition gratuita de su popular IDE JBuilder para Java. Este sitio también ofrece versiones de prueba de 30 días de las ediciones Enterprise y Developer. www.blueJ.org

BlueJ: una herramienta gratuita diseñada para ayudar a enseñar Java orientado a objetos a los programadores novatos. www.jgrasp.org

Descargas, documentación y tutoriales sobre jGRASP. Esta herramienta muestra representaciones visuales de programas en Java, para ayudar a su comprensión. www.jedit.org

jEdit: un editor de texto escrito en Java. developers.sun.com/prodtech/javatools/jsenterprise/index.jsp

El IDE Sun Java Studio Enterprise: la versión mejorada de NetBeans de Sun Microsystems.

www.elsolucionario.net

Resumen

27

www.jcreator.com

JCreator: un IDE popular para Java. JCreator Lite Edition está disponible como descarga gratuita. También está disponible una versión de prueba de 30 días de JCreator Pro Edition. www.textpad.com

TextPad: compile, edite y ejecute sus programas en Java desde este editor, que proporciona coloreo de sintaxis y una interfaz fácil de usar. www.download.com

Un sitio que contiene descargas de aplicaciones de freeware y shareware, incluyendo programas editores.

Sitios de recursos adicionales sobre Java www.javalobby.org

Proporciona noticias actualizadas sobre Java, foros en donde los desarrolladores pueden intercambiar tips y consejos, y una base de conocimiento de Java extensa, que organiza artículos y descargas en toda la Web. www.jguru.com

Ofrece foros, descargas, artículos, cursos en línea y una extensa colección de FAQs (Preguntas frecuentes) sobre Java. www.javaworld.com

Ofrece recursos para desarrolladores de Java, como artículos, índices de libros populares sobre Java, tips y FAQs. www.ftponline.com/javapro

La revista JavaPro contiene artículos mensuales, tips de programación, reseñas de libros y mucho más. sys-con.com/java/

El Diario de Desarrolladores de Java de Sys-Con Media ofrece artículos, libros electrónicos y otros recursos sobre Java.

Resumen Sección 1.1 Introducción • Java se ha convertido en el lenguaje de elección para implementar aplicaciones basadas en Internet y software para dispositivos que se comunican a través de una red. • Java Enterprise Edition (Java EE) está orientada hacia el desarrollo de aplicaciones de redes distribuidas de gran escala, y aplicaciones basadas en Web. • Java Micro Edition (Java ME) está orientada hacia el desarrollo de aplicaciones para dispositivos pequeños, con memoria limitada, como teléfonos celulares, radiolocalizadotes y PDAs.

Sección 1.2 ¿Qué es una computadora? • Una computadora es un dispositivo capaz de realizar cálculos y tomar decisiones lógicas a velocidades de millones (incluso de miles de millones) de veces más rápidas que los humanos. • Las computadoras procesan los datos bajo el control de conjuntos de instrucciones llamadas programas de cómputo. Los programas guían a las computadoras a través de acciones especificadas por gente llamada programadores de computadoras. • Una computadora está compuesta por varios dispositivos conocidos como hardware. A los programas que se ejecutan en una computadora se les denomina software.

Sección 1.3 Organización de una computadora • Casi todas las computadoras pueden representarse mediante seis unidades lógicas o secciones. • La unidad de entrada obtiene información desde los dispositivos de entrada y pone esta información a disposición de las otras unidades para que pueda procesarse. • La unidad de salida toma información que ya ha sido procesada por la computadora y la coloca en los diferentes dispositivos de salida, para que esté disponible fuera de la computadora. • La unidad de memoria es la sección de “almacén” de acceso rápido, pero con relativa baja capacidad, de la computadora. Retiene la información que se introduce a través de la unidad de entrada, para que la información pueda estar disponible de manera inmediata para procesarla cuando sea necesario. También retiene la información procesada hasta que ésta pueda ser colocada en los dispositivos de salida por la unidad de salida. • La unidad aritmética y lógica (ALU) es la responsable de realizar cálculos (como suma, resta, multiplicación y división) y tomar decisiones.

www.elsolucionario.net

28

Capítulo 1

Introducción a las computadoras, Internet y Web

• La unidad central de procesamiento (CPU) coordina y supervisa la operación de las demás secciones. La CPU le indica a la unidad de entrada cuándo debe grabarse la información dentro de la unidad de memoria, a la ALU cuándo debe utilizarse la información de la unidad de memoria para los cálculos, y a la unidad de salida cuándo enviar la información desde la unidad de memoria hasta ciertos dispositivos de salida. • Los multiprocesadores contienen múltiples CPUs y, por lo tanto, pueden realizar muchas operaciones de manera simultánea. • La unidad de almacenamiento secundario es la sección de “almacén” de alta capacidad y de larga duración de la computadora. Los programas o datos que no se encuentran en ejecución por las otras unidades, normalmente se colocan en dispositivos de almacenamiento secundario hasta que son requeridos de nuevo.

Sección 1.4 Los primeros sistemas operativos • • • •

Las primeras computadoras eran capaces de realizar solamente una tarea o trabajo a la vez. Los sistemas operativos se desarrollaron para facilitar el uso de la computadora. La multiprogramación significa la operación simultánea de muchas tareas. Con el tiempo compartido, la computadora ejecuta una pequeña porción del trabajo de un usuario y después procede a dar servicio al siguiente usuario, con la posibilidad de proporcionar el servicio a cada usuario varias veces por segundo.

Sección 1.5 Computación personal, distribuida y cliente/servidor • En 1977, Apple Computer popularizó el fenómeno de la computación personal. • En 1981, IBM, el vendedor de computadoras más grande del mundo, introdujo la Computadora Personal (PC) de IBM, que legitimó rápidamente la computación en las empresas, en la industria y en las organizaciones gubernamentales. • En la computación distribuida, en vez de que la computación se realice sólo en una computadora central, se distribuye mediante redes a los sitios en donde se realiza el trabajo de la empresa. • Los servidores almacenan datos que pueden utilizar las computadoras cliente distribuidas a través de la red, de ahí el término de computación cliente/servidor. • Java se está utilizando ampliamente para escribir software para redes de computadoras y para aplicaciones cliente/ servidor distribuidas.

Sección 1.6 Internet y World Wide Web • Internet es accesible por más de mil millones de computadoras y dispositivos controlados por computadora. • Con la introducción de World Wide Web, Internet se ha convertido explosivamente en uno de los principales mecanismos de comunicación en todo el mundo.

Sección 1.7 Lenguajes máquina, lenguajes ensambladores y lenguajes de alto nivel • Cualquier computadora puede entender de manera directa sólo su propio lenguaje máquina. • El lenguaje máquina es el “lenguaje natural” de una computadora. • Por lo general, los lenguajes máquina consisten en cadenas de números (que finalmente se reducen a 1s y 0s) que instruyen a las computadoras para realizar sus operaciones más elementales, una a la vez. • Los lenguajes máquina son dependientes de la máquina. • Los programadores empezaron a utilizar abreviaturas del inglés para representar las operaciones elementales. Estas abreviaturas formaron la base de los lenguajes ensambladores. • Los programas traductores conocidos como ensambladores se desarrollaron para convertir los primeros programas en lenguaje ensamblador a lenguaje máquina, a la velocidad de la computadora. • Los lenguajes de alto nivel permiten a los programadores escribir instrucciones parecidas al lenguaje inglés cotidiano, y contienen notaciones matemáticas de uso común. • Java es el lenguaje de programación de alto nivel más utilizado en todo el mundo. • Los programas intérpretes ejecutan los programas en lenguajes de alto nivel directamente.

Sección 1.8 Historia de C y C++ • Java evolucionó de C++, el cual evolucionó de C, que a su vez evolucionó de BCPL y B. • El lenguaje C evolucionó a partir de B, gracias al trabajo de Dennis Ritchie en los laboratorios Bell. Inicialmente, se hizo muy popular como lenguaje de desarrollo para el sistema operativo UNIX. • A principios de la década de los ochenta, Bjarne Stroustrup desarrolló una extensión de C en los laboratorios Bell: C++. Este lenguaje proporciona un conjunto de características que “pulen” al lenguaje C, además de la capacidad de una programación orientada a objetos.

www.elsolucionario.net

Resumen

29

Sección 1.9 Historia de Java • Java se utiliza para desarrollar aplicaciones empresariales a gran escala, para mejorar la funcionalidad de los servidores Web, para proporcionar aplicaciones para los dispositivos domésticos y para muchos otros propósitos. • Los programas en Java consisten en piezas llamadas clases. Las clases incluyen piezas llamadas métodos, los cuales realizan tareas y devuelven información cuando se completan estas tareas.

Sección 1.10 Bibliotecas de clases de Java • La mayoría de los programadores en Java aprovechan las ricas colecciones de clases existentes en las bibliotecas de clases de Java, que también se conocen como APIs (Interfaces de programación de aplicaciones) de Java. • La ventaja de crear sus propias clases y métodos es que sabe cómo funcionan y puede examinar el código. La desventaja es que se requiere una cantidad considerable de tiempo y un esfuerzo potencialmente complejo.

Sección 1.11 FORTRAN, COBOL, Pascal y Ada • Fortran (FORmula TRANslator, Traductor de fórmulas) fue desarrollado por IBM Corporation a mediados de la década de los cincuenta para utilizarse en aplicaciones científicas y de ingeniería que requerían cálculos matemáticos complejos. • COBOL (COmmon Business Oriented Language, Lenguaje común orientado a negocios) se utiliza en aplicaciones comerciales que requieren de una manipulación precisa y eficiente de grandes volúmenes de datos. • Las actividades de investigación en la década de los sesenta dieron como resultado la evolución de la programación estructurada (un método disciplinado para escribir programas que sean más claros, fáciles de probar y depurar, y más fáciles de modificar que los programas extensos producidos con técnicas anteriores). • Pascal se diseñó para la enseñanza de la programación estructurada en ambientes académicos, y de inmediato se convirtió en el lenguaje de programación preferido en la mayoría de las universidades. • El lenguaje de programación Ada se desarrolló bajo el patrocinio del Departamento de Defensa de los Estados Unidos (DOD) para satisfacer la mayoría de sus necesidades. Una característica de Ada conocida como multitarea permite a los programadores especificar que muchas actividades ocurrirán en paralelo. Java, a través de una técnica que se conoce como subprocesamiento múltiple, también permite a los programadores escribir programas con actividades paralelas.

Sección 1.12 BASIC, Visual Basic, Visual C++, C# y .NET • BASIC fue desarrollado a mediados de la década de los sesenta para escribir programas simples. • El lenguaje Visual Basic de Microsoft simplifica el desarrollo de aplicaciones para Windows. • La plataforma .NET de Microsoft integra Internet y Web en las aplicaciones de computadora.

Sección 1.13 Entorno de desarrollo típico en Java • Por lo general, los programas en Java pasan a través de cinco fases: edición, compilación, carga, verificación y ejecución. • La fase 1 consiste en editar un archivo con un editor. Usted escribe un programa utilizando el editor, realiza las correcciones necesarias y guarda el programa en un dispositivo de almacenamiento secundario, tal como su disco duro. • Un nombre de archivo que termina con la extensión .java indica que éste contiene código fuente en Java. • Los entornos de desarrollo integrados (IDEs) proporcionan herramientas que dan soporte al proceso de desarrollo del software, incluyendo editores para escribir y editar programas, y depuradores para localizar errores lógicos. • En la fase 2, el programador utiliza el comando javac para compilar un programa. • Si un programa se compila, el compilador produce un archivo .class que contiene el programa compilado. • El compilador de Java traduce el código fuente de Java en códigos de bytes que representan las tareas a ejecutar. La Máquina Virtual de Java (JVM) ejecuta los códigos de bytes. • En la fase 3, de carga, el cargador de clases toma los archivos .class que contienen los códigos de bytes del programa y los transfiere a la memoria principal. • En la fase 4, a medida que se cargan las clases, el verificador de códigos de bytes examina sus códigos de bytes para asegurar que sean válidos y que no violen las restricciones de seguridad de Java. • En la fase 5, la JVM ejecuta los códigos de bytes del programa.

Sección 1.16 Ejemplo práctico de Ingeniería de Software: introducción a la tecnología de objetos y UML • El Lenguaje Unificado de Modelado (UML) es un lenguaje gráfico que permite a las personas que crean sistemas representar sus diseños orientados a objetos en una notación común.

www.elsolucionario.net

30

Capítulo 1

Introducción a las computadoras, Internet y Web

• El diseño orientado a objetos (DOO) modela los componentes de software en términos de objetos reales. • Los objetos tienen la propiedad de ocultamiento de la información: por lo general, no se permite a los objetos de una clase saber cómo se implementan los objetos de otras clases. • La programación orientada a objetos (POO) implementa diseños orientados a objetos. • Los programadores de Java se concentran en crear sus propios tipos definidos por el usuario, conocidos como clases. Cada clase contiene datos y métodos que manipulan a esos datos y proporcionan servicios a los clientes. • Los componentes de datos de una clase son los atributos o campos; los componentes de operación son los métodos. • Las clases pueden tener relaciones con otras clases; a estas relaciones se les llama asociaciones. • El proceso de empaquetar software en forma de clases hace posible que los sistemas de software posteriores reutilicen esas clases. • A una instancia de una clase se le llama objeto. • El proceso de analizar y diseñar un sistema desde un punto de vista orientado a objetos se llama análisis y diseño orientados a objetos (A/DOO).

Terminología Ada ALU (unidad aritmética y lógica) ANSI C API de Java (Interfaz de Programación de Aplicaciones) atributo BASIC bibliotecas de clases C C# C++ cargador de clases clase .class, archivo COBOL código de bytes compilador compilador HotSpot™ compilador JIT (justo a tiempo) componente reutilizable comportamiento computación cliente/servidor computación distribuida computación personal computadora contenido dinámico CPU (unidad central de procesamiento) disco diseño orientado a objetos (DOO) dispositivo de entrada dispositivo de salida documento de requerimientos editor encapsulamiento ensamblador entrada/salida (E/S) enunciado del problema error en tiempo de compilación error en tiempo de ejecución error fatal en tiempo de ejecución

error no fatal en tiempo de ejecución fase de carga fase de compilación fase de edición fase de ejecución fase de verificación flujo de datos Fortran Hardware herencia HTML (Lenguaje de Marcado de Hipertexto) IDE (Entorno Integrado de Desarrollo) Internet Intérprete Java Java Enterprise Edition (Java EE) .java, extensión de nombre de archivo Java Micro Edition (Java ME) Java Standard Edition (Java SE) java, intérprete javac, compilador KIS (simplifíquelo) Kit de Desarrollo de Java (JDK) LAN (red de área local) lenguaje de alto nivel lenguaje ensamblador lenguaje máquina Lenguaje Unificado de Modelado (UML) Máquina virtual de Java (JVM) memoria principal método método de código activo (live-code) Microsoft Internet Explorer, navegador Web modelado multiprocesador multiprogramación .NET objeto ocultamiento de información

www.elsolucionario.net

Ejercicios de autoevaluación Pascal plataforma portabilidad programa de cómputo programa traductor programación estructurada programación orientada a objetos (POO) programación por procedimientos programador de computadoras pseudocódigo reutilización de software servidor de archivos sistema heredado sistema operativo software

31

subprocesamiento múltiple Sun Microsystems tiempo compartido tipo definido por el usuario traducción unidad aritmética y lógica (ALU) unidad central de procesamiento (CPU) unidad de almacenamiento secundario unidad de entrada unidad de memoria unidad de salida verificador de código de bytes Visual Basic .NET Visual C++ .NET World Wide Web

Ejercicios de autoevaluación 1.1

Complete las siguientes oraciones: a) La compañía que popularizó la computación personal fue ______________. b) La computadora que legitimó la computación personal en los negocios y la industria fue _____________. c) Las computadoras procesan los datos bajo el control de conjuntos de instrucciones llamadas ___________. d) Las seis unidades lógicas clave de la computadora son _____________, _____________, _____________, _____________, _____________ y _____________. e) Los tres tipos de lenguajes descritos en este capítulo son _____________, _____________ y __________ ____________________________. f ) Los programas que traducen programas en lenguaje de alto nivel a lenguaje máquina se denominan _____ _____________. g) La __________ permite a los usuarios de computadora localizar y ver documentos basados en multimedia sobre casi cualquier tema, a través de Internet. h) _____________, permite a un programa en Java realizar varias actividades en paralelo.

1.2

Complete cada una de las siguientes oraciones relacionadas con el entorno de Java: a) El comando _____________ del JDK ejecuta una aplicación en Java. b) El comando _____________ del JDK compila un programa en Java. c) El archivo de un programa en Java debe terminar con la extensión de archivo _____________. d) Cuando se compila un programa en Java, el archivo producido por el compilador termina con la extensión _____________. e) El archivo producido por el compilador de Java contiene _____________ que se ejecutan mediante la Máquina Virtual de Java.

1.3

Complete cada una de las siguientes oraciones (basándose en la sección 1.16): a) Los objetos tienen una propiedad que se conoce como _____________; aunque éstos pueden saber cómo comunicarse con los demás objetos a través de interfaces bien definidas, generalmente no se les permite saber cómo están implementados los otros objetos. b) Los programadores de Java se concentran en crear _____________, que contienen campos y el conjunto de métodos que manipulan a esos campos y proporcionan servicios a los clientes. c) Las clases pueden tener relaciones con otras clases; a éstas relaciones se les llama _____________. d) El proceso de analizar y diseñar un sistema desde un punto de vista orientado a objetos se conoce como _____________ . e) El DOO aprovecha las relaciones _____________, en donde se derivan nuevas clases de objetos al absorber las características de las clases existentes y después agregar sus propias características únicas. f ) _____________ es un lenguaje gráfico que permite a las personas que diseñan sistemas de software utilizar una notación estándar en la industria para representarlos. g) El tamaño, forma, color y peso de un objeto se consideran _____________ del mismo.

www.elsolucionario.net

32

Capítulo 1

Introducción a las computadoras, Internet y Web

Respuestas a los ejercicios de autoevaluación 1.1 a) Apple. b) PC de IBM. c) programas. d) unidad de entrada, unidad de salida, unidad de memoria, unidad aritmética y lógica, unidad central de procesamiento, unidad de almacenamiento secundario. e) lenguajes máquina, lenguajes ensambladores, lenguajes de alto nivel. f ) compiladores. g) World Wide Web. h) Subprocesamiento múltiple. 1.2

a) java. b) javac. c) .java. d) .class. e) códigos de bytes.

1.3 a) ocultamiento de información. b) clases. c) asociaciones. d) análisis y diseño orientados a objetos (A/ DOO). e) herencia. f ) El Lenguaje Unificado de Modelado (UML). g) atributos.

Ejercicios 1.4

Clasifique cada uno de los siguientes elementos como hardware o software: a) CPU b) compilador de Java c) JVM d) unidad de entrada e) editor

1.5

Complete cada una de las siguientes oraciones: a) La unidad lógica de la computadora que recibe información desde el exterior de la computadora para que ésta la utilice se llama _____________. b) El proceso de indicar a la computadora cómo resolver problemas específicos se llama _____________. c) _____________ es un tipo de lenguaje computacional que utiliza abreviaturas del inglés para las instrucciones de lenguaje máquina. d) _____________ es una unidad lógica de la computadora que envía información, que ya ha sido procesada por la computadora, a varios dispositivos, de manera que la información pueda utilizarse fuera de la computadora. e) _____________ y _____________ son unidades lógicas de la computadora que retienen información. f ) _____________ es una unidad lógica de la computadora que realiza cálculos. g) _____________ es una unidad lógica de la computadora que toma decisiones lógicas. h) Los lenguajes _____________ son los más convenientes para que el programador pueda escribir programas rápida y fácilmente. i) Al único lenguaje que una computadora puede entender directamente se le conoce como el __________ de esa computadora. j) _____________ es una unidad lógica de la computadora que coordina las actividades de todas las demás unidades lógicas.

1.6 Indique la diferencia entre los términos error fatal y error no fatal. ¿Por qué sería preferible experimentar un error fatal, en vez de un error no fatal? 1.7

Complete cada una de las siguientes oraciones: a) _____________ se utiliza ahora para desarrollar aplicaciones empresariales de gran escala, para mejorar la funcionalidad de los servidores Web, para proporcionar aplicaciones para dispositivos domésticos y para muchos otros fines más. b) _____________ se diseñó específicamente para la plataforma .NET, de manera que los programadores pudieran migrar fácilmente a .NET. c) Inicialmente, _____________ se hizo muy popular como lenguaje de desarrollo para el sistema operativo UNIX. d) _____________ fue desarrollado a mediados de la década de los sesenta en el Dartmouth College, como un medio para escribir programas simples. e) _____________ fue desarrollado por IBM Corporation a mediados de la década de los cincuenta para utilizarse en aplicaciones científicas y de ingeniería que requerían cálculos matemáticos complejos. f ) _____________ se utiliza para aplicaciones comerciales que requieren la manipulación precisa y eficiente de grandes cantidades de datos.

www.elsolucionario.net

Ejercicios

33

g) El lenguaje de programación _____________ fue desarrollado por Bjarne Stroustrup a principios de la década de los ochenta, en los laboratorios Bell. 1.8

Complete cada una de las siguientes oraciones (basándose en la sección 1.13): a) Por lo general, los programas de Java pasan a través de cinco fases: _____________, _____________, _____________, _____________ y _____________. b) Un _____________ proporciona muchas herramientas que dan soporte al proceso de desarrollo de software, como los editores para escribir y editar programas, los depuradores para localizar los errores lógicos en los programas, y muchas otras características más. c) El comando java invoca al _____________, que ejecuta los programas de Java. d) Un(a) _____________ es una aplicación de software que simula a una computadora, pero oculta el sistema operativo subyacente y el hardware de los programas que interactúan con la VM. e) Un programa _____________ puede ejecutarse en múltiples plataformas. f ) El _____________ toma los archivos .class que contienen los códigos de bytes del programa y los transfiere a la memoria principal. g) El _____________ examina los códigos de bytes para asegurar que sean válidos.

1.9

Explique las dos fases de compilación de los programas de Java.

www.elsolucionario.net

2 Introducción a las aplicaciones en Java ¿Qué hay en un nombre? A eso a lo que llamamos rosa, si le diéramos otro nombre conservaría su misma fragancia dulce. — William Shakespeare

OBJETIVOS En este capítulo aprenderá a: Q

Escribir aplicaciones simples en Java.

Q

Utilizar las instrucciones de entrada y salida.

Q

Familiarizarse con los tipos primitivos de Java.

Q

Comprender los conceptos básicos de la memoria.

Q

Utilizar los operadores aritméticos.

Q

Comprender la precedencia de los operadores aritméticos.

Q

Escribir instrucciones para tomar decisiones.

Q

Utilizar los operadores relacionales y de igualdad.

Al hacer frente a una decisión, siempre me pregunto, “¿Cuál será la solución más divertida?” —Peggy Walker

“Toma un poco más de té”, dijo el conejo blanco a Alicia, con gran seriedad. “No he tomado nada todavía.” Contestó Alicia en tono ofendido, “Entonces no puedo tomar más”. “Querrás decir que no puedes tomar menos”, dijo el sombrerero loco, “es muy fácil tomar más que nada”. —Lewis Carroll

www.elsolucionario.net

Pla n g e ne r a l

2.2

2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 2.10

Su primer programa en Java: imprimir una línea de texto

35

Introducción Su primer programa en Java: imprimir una línea de texto Modificación de nuestro primer programa en Java Cómo mostrar texto con printf Otra aplicación en Java: suma de enteros Conceptos acerca de la memoria Aritmética Toma de decisiones: operadores de igualdad y relacionales (Opcional) Ejemplo práctico de Ingeniería de Software: cómo examinar el documento de requerimientos Conclusión

Resumen | Terminología | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

2.1 Introducción Ahora presentaremos la programación de aplicaciones en Java, que facilita una metodología disciplinada para el diseño de programas. La mayoría de los programas en Java que estudiará en este libro procesan información y muestran resultados. Le presentaremos seis ejemplos que demuestran cómo sus programas pueden mostrar mensajes y cómo pueden obtener información del usuario para procesarla. Comenzaremos con varios ejemplos que simplemente muestran mensajes en la pantalla. Después demostraremos un programa que obtiene dos números de un usuario, calcula su suma y muestra el resultado. Usted aprenderá a realizar varios cálculos aritméticos y a guardar sus resultados para usarlos más adelante. El último ejemplo en este capítulo demuestra los fundamentos de toma de decisiones, al mostrarle cómo comparar números y después mostrar mensajes con base en los resultados de la comparación. Por ejemplo, el programa muestra un mensaje que indica que dos números son iguales sólo si tienen el mismo valor. Analizaremos cada ejemplo, una línea a la vez, para ayudarle a aprender a programar en Java. En los ejercicios del capítulo proporcionamos muchos problemas retadores y divertidos, para ayudarle a aplicar las habilidades que aprenderá aquí.

2.2 Su primer programa en Java: imprimir una línea de texto Cada vez que utiliza una computadora, ejecuta diversas aplicaciones que realizan tareas por usted. Por ejemplo, su aplicación de correo electrónico le permite enviar y recibir mensajes de correo, y su navegador Web le permite ver páginas de sitios Web en todo el mundo. Los programadores de computadoras crean dichas aplicaciones, escribiendo programas de cómputo. Una aplicación en Java es un programa de computadora que se ejecuta cuando usted utiliza el comando java para iniciar la Máquina Virtual de Java (JVM). Consideremos una aplicación simple que muestra una línea de texto. (Más adelante en esta sección hablaremos sobre cómo compilar y ejecutar una aplicación). El programa y su salida se muestran en la figura 2.1. La salida aparece en el recuadro al final del programa. El programa ilustra varias características importantes del lenguaje. Java utiliza notaciones que pueden parecer extrañas a los no programadores. Además, cada uno de los programas que presentamos en este libro tiene números de línea incluidos para su conveniencia; los números de línea no son parte de los programas en Java. Pronto veremos que la línea 9 se encarga del verdadero trabajo del programa; a saber, mostrar la frase Bienvenido a la programacion en Java! en la pantalla. Ahora consideremos cada línea del programa en orden. La línea 1 // Fig. 2.1: Bienvenido1.java

empieza con //, indicando que el resto de la línea es un comentario. Los programadores insertan comentarios para documentar los programas y mejorar su legibilidad. Los comentarios también ayudan a otras personas a leer y comprender un programa. El compilador de Java ignora estos comentarios, de manera que la computadora no hace nada cuando el programa se ejecuta. Por convención, comenzamos cada uno de los programas con un comentario, el cual indica el número de figura y el nombre del archivo.

www.elsolucionario.net

36

1 2 3 4 5 6 7 8 9 10 11 12 13

Capítulo 2

Introducción a las aplicaciones en Java

// Fig. 2.1: Bienvenido1.java // Programa para imprimir texto. public class Bienvenido1 { // el método main empieza la ejecución de la aplicación en Java public static void main( String args[] ) { System.out.println( "Bienvenido a la programacion en Java!" ); } // fin del método main } // fin de la clase Bienvenido1

Bienvenido a la programacion en Java!

Figura 2.1 | Programa para imprimir texto.

Un comentario que comienza con // se llama comentario de fin de línea (o de una sola línea), ya que termina al final de la línea en la que aparece. Un comentario que se especifica con // puede empezar también en medio de una línea, y continuar solamente hasta el final de esa línea (como en las líneas 11 y 13). Los comentarios tradicionales (también conocidos como comentarios de múltiples líneas), como el que se muestra a continuación /* Éste es un comentario Tradicional. Puede dividirse en muchas líneas */

se distribuyen en varias líneas. Este tipo de comentario comienza con el delimitador /* y termina con */. El compilador ignora todo el texto que esté entre los delimitadores. Java incorporó los comentarios tradicionales y los comentarios de fin de línea de los lenguajes de programación C y C++, respectivamente. En este libro utilizamos comentarios de fin de línea. Java también cuenta con comentarios Javadoc, que están delimitados por /** y */. Al igual que con los comentarios tradicionales, el compilador ignora todo el texto entre los delimitadores de los comentarios Javadoc. Estos comentarios permiten a los programadores incrustar la documentación del programa directamente en éste. Dichos comentarios son el formato preferido en la industria. El programa de utilería javadoc (parte del Kit de Desarrollo de Java SE) lee esos comentarios y los utiliza para preparar la documentación de su programa, en formato HTML. Hay algunas sutilezas en cuanto al uso apropiado de los comentarios estilo Java. En el apéndice K, Creación de documentación con javadoc, demostramos el uso de los comentarios Javadoc y la herramienta javadoc. Para obtener información completa, visite la página de herramientas de javadoc de Sun en java.sun. com/javase/6/docs/technotes/guides/javadoc/index.html.

Error común de programación 2.1 Olvidar uno de los delimitadores de un comentario tradicional o Javadoc es un error de sintaxis. La sintaxis de un lenguaje de programación que especifica las reglas para crear un programa apropiado en ese lenguaje. Un error de sintaxis ocurre cuando el compilador encuentra código que viola las reglas del lenguaje Java (es decir, su sintaxis). En este caso, el compilador muestra un mensaje de error para ayudar al programador a identificar y corregir el código incorrecto. Los errores de sintaxis se conocen también como errores del compilador, errores en tiempo de compilación o errores de compilación, ya que el compilador los detecta durante la fase de compilación. Usted no podrá ejecutar su programa sino hasta que corrija todos los errores de sintaxis que éste contenga.

La línea 2 // Programa para imprimir texto.

es un comentario de fin de línea que describe el propósito del programa.

www.elsolucionario.net

2.2

Su primer programa en Java: imprimir una línea de texto

37

Buena práctica de programación 2.1 Es conveniente que todo programa comience con un comentario que explique su propósito, el autor, la fecha y la hora de la última modificación del mismo. (No mostraremos el autor, la fecha y la hora en los programas de este libro, ya que sería redundante).

La línea 3 es una línea en blanco. Los programadores usan líneas en blanco y espacios para facilitar la lectura de los programas. En conjunto, las líneas en blanco, los espacios y los tabuladores se conocen como espacio en blanco. (Los espacios y tabuladores se conocen específicamente como caracteres de espacio en blanco). El compilador ignora el espacio en blanco. En éste y en los siguientes capítulos, hablaremos sobre las convenciones para utilizar espacios en blanco para mejorar la legibilidad de los programas.

Buena práctica de programación 2.2 Utilice líneas en blanco y espacios para mejorar la legibilidad del programa.

La línea 4 public class Bienvenido1

comienza una declaración de clase para la clase Bienvenido1. Todo programa en Java consiste de, cuando menos, una declaración de clase que usted, el programador, debe definir. Estas clases se conocen como clases definidas por el programador o clases definidas por el usuario. La palabra clave class introduce una declaración de clase en Java, la cual debe ir seguida inmediatamente por el nombre de la clase (Bienvenido1). Las palabras clave (algunas veces conocidas como palabras reservadas) se reservan para uso exclusivo de Java (hablaremos sobre las diversas palabras clave a lo largo de este texto) y siempre se escriben en minúscula. En el apéndice C se muestra la lista completa de palabras clave de Java. Por convención, todos los nombres de clases en Java comienzan con una letra mayúscula, y la primera letra de cada palabra en el nombre de la clase debe ir en mayúscula (por ejemplo, EjemploDeNombreDeClase). En Java, el nombre de una clase se conoce como identificador: una serie de caracteres que pueden ser letras, dígitos, guiones bajos ( _ ) y signos de moneda ($), que no comience con un dígito ni tenga espacios. Algunos identificadores válidos son Bienvenido1, $valor, _valor, m_campoEntrada1 y boton7. El nombre 7boton no es un identificador válido, ya que comienza con un dígito, y el nombre campo entrada tampoco lo es debido a que contiene un espacio. Por lo general, un identificador que no empieza con una letra mayúscula no es el nombre de una clase. Java es sensible a mayúsculas y minúsculas; es decir, las letras mayúsculas y minúsculas son distintas, por lo que a1 y A1 son distintos identificadores (pero ambos son válidos).

Buena práctica de programación 2.3 Por convención, el identificador del nombre de una clase siempre debe comenzar con una letra mayúscula, y la primera letra de cada palabra subsiguiente del identificador también debe ir en mayúscula. Los programadores de Java saben que, por lo general, dichos identificadores representan clases de Java, por lo que si usted nombra a sus clases de esta forma, sus programas serán más legibles.

Error común de programación 2.2 Java es sensible a mayúsculas y minúsculas. No utilizar la combinación apropiada de letras minúsculas y mayúsculas para un identificador, generalmente produce un error de compilación.

En los capítulos 2 al 7, cada una de las clases que definimos comienza con la palabra clave public. Cuando usted guarda su declaración de clase public en un archivo, el nombre del mismo debe ser el nombre de la clase, seguido de la extensión de nombre de archivo .java. Para nuestra aplicación, el nombre del archivo es Bienvenido1.java. En el capítulo 8 aprenderá más acerca de las clases public y las que no son public.

Error común de programación 2.3 Una clase public debe colocarse en un archivo que tenga el mismo nombre que la clase (en términos de ortografía y uso de mayúsculas) y la extensión .java; en caso contrario, ocurre un error de compilación.

www.elsolucionario.net

38

Capítulo 2

Introducción a las aplicaciones en Java

Error común de programación 2.4 Es un error que un archivo que contiene la declaración de una clase, no finalice con la extensión .java. El compilador de Java sólo compila archivos con la extensión .java.

Una llave izquierda (en la línea 5 de este programa), {, comienza el cuerpo de todas las declaraciones de clases. Su correspondiente llave derecha (en la línea 13), }, debe terminar cada declaración de una clase. Observe que las líneas de la 6 a la 11 tienen sangría; ésta es una de las convenciones de espaciado que se mencionaron anteriormente. Definimos cada una de las convenciones de espaciado como una Buena práctica de programación.

Buena práctica de programación 2.4 Siempre que escriba una llave izquierda de apertura ({ ) en su programa, escriba inmediatamente la llave derecha de cierre (}) y luego vuelva a colocar el cursor entre las llaves y utilice sangría para comenzar a escribir el cuerpo. Esta práctica ayuda a evitar errores debido a la omisión de una de las llaves.

Buena práctica de programación 2.5 Aplique sangría a todo el cuerpo de la declaración de cada clase, usando un “nivel” de sangría entre la llave izquierda ({ ) y la llave derecha (}), las cuales delimitan el cuerpo de la clase. Este formato enfatiza la estructura de la declaración de la clase, y facilita su lectura.

Buena práctica de programación 2.6 Establezca una convención para el tamaño de sangría que usted prefiera, y después aplique uniformemente esta convención. La tecla Tab puede utilizarse para crear sangrías, pero las posiciones de los tabuladores pueden variar entre los diversos editores de texto. Le recomendamos utilizar tres espacios para formar un nivel de sangría.

Error común de programación 2.5 Es un error de sintaxis no utilizar las llaves por pares.

La línea 6 // el método main empieza la ejecución de la aplicación en Java

es un comentario de fin de línea que indica el propósito de las líneas 7 a 11 del programa. La línea 7 public static void main( String args[] )

es el punto de inicio de toda aplicación en Java. Los paréntesis después del identificador main indican que éste es un bloque de construcción del programa, al cual se le llama método. Las declaraciones de clases en Java generalmente contienen uno o más métodos. En una aplicación en Java, sólo uno de esos métodos debe llamarse main y debe definirse como se muestra en la línea 7; de no ser así, la JVM no ejecutará la aplicación. Los métodos pueden realizar tareas y devolver información una vez que las hayan concluido. La palabra clave void indica que este método realizará una tarea, pero no devolverá ningún tipo de información cuando complete su tarea. Más adelante veremos que muchos métodos devuelven información cuando finalizan sus tareas. Aprenderá más acerca de los métodos en los capítulos 3 y 6. Por ahora, simplemente copie la primera línea de main en sus aplicaciones en Java. En la línea 7, las palabras String args[] entre paréntesis son una parte requerida de la declaración del método main. Hablaremos sobre esto en el capítulo 7, Arreglos. La llave izquierda ({) en la línea 8 comienza el cuerpo de la declaración del método; su correspondiente llave derecha (}) debe terminar el cuerpo de esa declaración (línea 11 del programa). Observe que la línea 9, entre las llaves, tiene sangría.

Buena práctica de programación 2.7 Aplique un “nivel” de sangría a todo el cuerpo de la declaración de cada método, entre la llave izquierda ({) y la llave derecha (}), las cuales delimitan el cuerpo del método. Este formato resalta la estructura del método y ayuda a que su declaración sea más fácil de leer.

www.elsolucionario.net

2.2

Su primer programa en Java: imprimir una línea de texto

39

La línea 9 System.out.println( "Bienvenido a la programacion en Java!" );

indica a la computadora que realice una acción; es decir, que imprima la cadena de caracteres contenida entre los caracteres de comillas dobles (sin incluirlas). A una cadena también se le denomina cadena de caracteres, mensaje o literal de cadena. Genéricamente, nos referimos a los caracteres entre comillas dobles como cadenas. El compilador no ignora los caracteres de espacio en blanco dentro de las cadenas. System.out se conoce como el objeto de salida estándar. System.out permite a las aplicaciones en Java mostrar conjuntos de caracteres en la ventana de comandos, desde la cual se ejecuta la aplicación en Java. En Microsoft Windows 95/98/ME, la ventana de comandos es el símbolo de MS-DOS. En versiones más recientes de Microsoft Windows, la ventana de comandos es el Símbolo del sistema. En UNIX/Linux/Mac OS X, la ventana de comandos se llama ventana de terminal o shell. Muchos programadores se refieren a la ventana de comandos simplemente como la línea de comandos. El método System.out.println muestra (o imprime) una línea de texto en la ventana de comandos. La cadena dentro de los paréntesis en la línea 9 es el argumento para el método. El método System.out.println realiza su tarea, mostrando (o enviando) su argumento en la ventana de comandos. Cuando System.out. println completa su tarea, posiciona el cursor de salida (la ubicación en donde se mostrará el siguiente carácter) al principio de la siguiente línea en la ventana de comandos. [Este desplazamiento del cursor es similar a cuando un usuario oprime la tecla Intro, al escribir en un editor de texto (el cursor aparece al principio de la siguiente línea en el archivo)]. Toda la línea 9, incluyendo System.out.println, el argumento "Bienvenido a la programacion en Java!" entre paréntesis y el punto y coma (;), se conoce como una instrucción; y siempre debe terminar con un punto y coma. Cuando se ejecuta la instrucción de la línea 9 de nuestro programa, ésta muestra el mensaje Bienvenido a la programacion en Java! en la ventana de comandos. Por lo general, un método está compuesto por una o más instrucciones que realizan la tarea, como veremos en los siguientes programas.

Error común de programación 2.6 Omitir el punto y coma al final de una instrucción es un error de sintaxis.

Tip para prevenir errores 2.1 Al aprender a programar, es conveniente, en ocasiones, “descomponer” un programa funcional, para poder familiarizarse con los mensajes de error de sintaxis del compilador; ya que este tipo de mensajes no siempre indican el problema exacto en el código. Y de esta manera, cuando se encuentren dichos mensajes de error de sintaxis, tendrá una idea de qué fue lo que ocasionó el error. Trate de quitar un punto y coma o una llave del programa de la figura 2.1, y vuelva a compilarlo de manera que pueda ver los mensajes de error generados por esta omisión.

Tip para prevenir errores 2.2 Cuando el compilador reporta un error de sintaxis, éste tal vez no se encuentre en el número de línea indicado por el mensaje. Primero verifique la línea en la que se reportó el error; si esa línea no contiene errores de sintaxis, verifique las líneas anteriores.

A algunos programadores se les dificulta, cuando leen o escriben un programa, relacionar las llaves izquierda y derecha ({ y }) que delimitan el cuerpo de la declaración de una clase o de un método. Por esta razón, incluyen un comentario de fin de línea después de una llave derecha de cierre (}) que termina la declaración de un método y que termina la declaración de una clase. Por ejemplo, la línea 11 } // fin del método main

especifica la llave derecha de cierre (}) del método main, y la línea 13 } // fin de la clase Bienvenido1

especifica la llave derecha de cierre (}) de la clase Bienvenido1. Cada comentario indica el método o la clase que termina con esa llave derecha.

www.elsolucionario.net

40

Capítulo 2

Introducción a las aplicaciones en Java

Buena práctica de programación 2.8 Para mejorar la legibilidad de los programas, agregue un comentario de fin de línea después de la llave derecha de cierre (}), que indique a qué método o clase pertenece.

Cómo compilar y ejecutar su primera aplicación en Java Ahora estamos listos para compilar y ejecutar nuestro programa. Para este propósito, supondremos que usted utiliza el Kit de Desarrollo 6.0 (JDK 6.0) de Java SE de Sun Microsystems. En nuestros centros de recursos en www.deitel.com/ResourceCenters.html proporcionamos vínculos a tutoriales que le ayudarán a empezar a trabajar con varias herramientas de desarrollo populares de Java. Para compilar el programa, abra una ventana de comandos y cambie al directorio en donde está guardado el programa. La mayoría de los sistemas operativos utilizan el comando cd para cambiar directorios. Por ejemplo, cd c:\ejemplos\cap02\fig02_01

cambia al directorio fig02_01 en Windows. El comando cd ~/ejemplos/cap02/fig02_01

cambia al directorio fig02_01 en UNIX/Linux/Mac OS X. Para compilar el programa, escriba javac Bienvenido1.java

Si el programa no contiene errores de sintaxis, el comando anterior crea un nuevo archivo llamado Bienvenido1. class (conocido como el archivo de clase para Bienvenido1), el cual contiene los códigos de bytes de Java que representan nuestra aplicación. Cuando utilicemos el comando java para ejecutar la aplicación, la JVM ejecutará estos códigos de bytes.

Tip para prevenir errores 2.3 Cuando trate de compilar un programa, si recibe un mensaje como “comando o nombre de archivo incorrecto”, “javac: comando no encontrado” o “'javac ' no se reconoce como un comando interno o externo, programa o archivo por lotes ejecutable”, entonces su instalación del software Java no se completó apropiadamente. Con el JDK, esto indica que la variable de entorno PATH del sistema no se estableció apropiadamente. Consulte cuidadosamente las instrucciones de instalación en la sección Antes de empezar de este libro. En algunos sistemas, después de corregir la variable PATH, es probable que necesite reiniciar su equipo o abrir una nueva ventana de comandos para que estos ajustes tengan efecto.

Tip para prevenir errores 2.4 Cuando la sintaxis de un programa es incorrecta, el compilador de Java genera mensajes de error de sintaxis; éstos contienen el nombre de archivo y el número de línea en donde ocurrió el error. Por ejemplo, Bienvenido1.java:6 indica que ocurrió un error en el archivo Bienvenido1.java en la línea 6. El resto del mensaje proporciona información acerca del error de sintaxis.

Tip para prevenir errores 2.5 El mensaje de error del compilador “Public class NombreClase must be defined in a file called NombreClase.java” indica que el nombre del archivo no coincide exactamente con el nombre de la clase public en el archivo, o que escribió el nombre de la clase en forma incorrecta al momento de compilarla.

La figura 2.2 muestra el programa de la figura 2.1 ejecutándose en una ventana Símbolo del sistema de Microsoft® Windows® XP. Para ejecutar el programa, escriba java Bienvenido1; posteriormente se iniciará la JVM, que cargará el archivo “.class” para la clase Bienvenido1. Observe que la extensión “.class” del nombre de archivo se omite en el comando anterior; de no ser así, la JVM no ejecutaría el programa. La JVM llama al método main. A continuación, la instrucción de la línea 9 de main muestra "Bienvenido a la programacion en Java!" [Nota: muchos entornos muestran los símbolos del sistema con fondos negros y texto blanco. En nuestro entorno, ajustamos esta configuración para que nuestras capturas de pantalla fueran más legibles].

www.elsolucionario.net

2.3

Modificación de nuestro primer programa en Java

41

Usted escribe este comando para ejecutar la aplicación

El programa imprime en la pantalla Bienvenido a la programacion en Java!

Figura 2.2 | Ejecución de Bienvenido1 en una ventana Símbolo del sistema de Microsoft Windows XP.

Tip para prevenir errores 2.6 Al tratar de ejecutar un programa en Java, si recibe el mensaje “Exception in thread "main" java.1ang. NoC1assDefFoundError: Bienvenido1”, quiere decir que su variable de entorno CLASSPATH no se ha configurado apropiadamente. Consulte cuidadosamente las instrucciones de instalación en la sección Antes de empezar de este libro. En algunos sistemas, tal vez necesite reiniciar su equipo o abrir una nueva ventana de comandos para que estos ajustes tengan efecto.

2.3 Modificación de nuestro primer programa en Java Esta sección continúa con nuestra introducción a la programación en Java, con dos ejemplos que modifican el ejemplo de la figura 2.1 para imprimir texto en una línea utilizando varias instrucciones, y para imprimir texto en varias líneas utilizando una sola instrucción.

Cómo mostrar una sola línea de texto con varias instrucciones Bienvenido a la programacion en Java! puede mostrarse en varias formas. La clase Bienvenido2, que se muestra en la figura 2.3, utiliza dos instrucciones para producir el mismo resultado que el de la figura 2.1. De aquí en adelante, resaltaremos las características nuevas y las características clave en cada listado de código, como se muestra en las línea 9 a 10 de este programa.

1 2 3 4 5 6 7 8 9 10 11 12 13 14

// Fig. 2.3: Bienvenido2.java // Imprimir una línea de texto con varias instrucciones. public class Bienvenido2 { // el método main empieza la ejecución de la aplicación en Java public static void main( String args[] ) { System.out.print( "Bienvenido a " ); System.out.println( "la programacion en Java!" ); } // fin del método main } // fin de la clase Bienvenido2

Bienvenido a la programacion en Java!

Figura 2.3 | Impresión de una línea de texto con varias instrucciones.

www.elsolucionario.net

42

Capítulo 2

Introducción a las aplicaciones en Java

El programa es similar al de la figura 2.1, por lo que aquí sólo hablaremos de los cambios. La línea 2 // Imprimir una línea de texto con varias instrucciones.

es un comentario de fin de línea que describe el propósito de este programa. La línea 4 comienza la declaración de la clase Bienvenido2. Las líneas 9 y 10 del método main System.out.print( "Bienvenido a " ); System.out.println( "la programacion en Java!" );

muestran una línea de texto en la ventana de comandos. La primera instrucción utiliza el método print de System.out para mostrar una cadena. A diferencia de println, después de mostrar su argumento, print no posiciona el cursor de salida al inicio de la siguiente línea en la ventana de comandos; sino que el siguiente carácter aparecerá inmediatamente después del último que muestre print. Por lo tanto, la línea 10 coloca el primer carácter de su argumento (la letra “l”) inmediatamente después del último que muestra la línea 9 (el carácter de espacio antes del carácter de comilla doble de cierre de la cadena). Cada instrucción print o println continúa mostrando caracteres a partir de donde la última instrucción print o println dejó de mostrar caracteres.

Cómo mostrar varias líneas de texto con una sola instrucción Una sola instrucción puede mostrar varias líneas, utilizando caracteres de nueva línea, los cuales indican a los métodos print y println de System.out cuándo deben colocar el cursor de salida al inicio de la siguiente línea en la ventana de comandos. Al igual que las líneas en blanco, los espacios y los tabuladores, los caracteres de nueva línea son caracteres de espacio en blanco. La figura 2.4 muestra cuatro líneas de texto, utilizando caracteres de nueva línea para determinar cuándo empezar cada nueva línea. La mayor parte del programa es idéntico a los de las figuras 2.1 y 2.3, por lo que aquí sólo veremos los cambios. La línea 2 // Imprimir varias líneas de texto con una sola instrucción.

es un comentario que describe el propósito de este programa. La línea 4 comienza la declaración de la clase Bienvenido3. La línea 9 System.out.println( "Bienvenido\na\nla programacion\nenJava!" );

muestra cuatro líneas separadas de texto en la ventana de comandos. Por lo general, los caracteres en una cadena se muestran exactamente como aparecen en las comillas dobles. Sin embargo, observe que los dos caracteres 1 2 3 4 5 6 7 8 9 10 11 12 13

// Fig. 2.4: Bienvenido3.java // Imprimir varias líneas de texto con una sola instrucción. public class Bienvenido3 { // el método main empieza la ejecución de la aplicación en Java public static void main( String args[] ) { System.out.println( "Bienvenido\na\nla programacion\nen Java!" ); } // fin del método main } // fin de la clase Bienvenido3

Bienvenido a la programacion en Java!

Figura 2.4 | Impresión de varias líneas de texto con una sola instrucción.

www.elsolucionario.net

2.4

Secuencia de escape

Cómo mostrar texto con printf

43

Descripción

\n

Nueva línea. Coloca el cursor de la pantalla al inicio de la siguiente línea.

\t

Tabulador horizontal. Desplaza el cursor de la pantalla hasta la siguiente posición de tabulación.

\r

Retorno de carro. Coloca el cursor de la pantalla al inicio de la línea actual; no avanza a la siguiente línea. Cualquier carácter que se imprima después del retorno de carro sobrescribe los caracteres previamente impresos en esa línea.

\\

Barra diagonal inversa. Se usa para imprimir un carácter de barra diagonal inversa.

\”

Doble comilla. Se usa para imprimir un carácter de doble comilla. Por ejemplo, System.out.println( "\"entre comillas\"" );

muestra "entre comillas"

Figura 2.5 | Algunas secuencias de escape comunes.

\ y n (que se repiten tres veces en la instrucción) no aparecen en la pantalla. La barra diagonal inversa (\) se conoce como carácter de escape. Este carácter indica a los métodos print y println de System.out que se va a imprimir un “carácter especial”. Cuando aparece una barra diagonal inversa en una cadena de caracteres, Java combina el siguiente carácter con la barra diagonal inversa para formar una secuencia de escape. La secuencia de escape \n representa el carácter de nueva línea. Cuando aparece un carácter de nueva línea en una cadena que se va a imprimir con System.out, el carácter de nueva línea hace que el cursor de salida de la pantalla se desplace al inicio de la siguiente línea en la ventana de comandos. En la figura 2.5 se enlistan varias secuencias de escape comunes, con descripciones de cómo afectan la manera de mostrar caracteres en la ventana de comandos. Para obtener una lista completa de secuencias de escape, visite java.sun.com/docs/books/jls/third_edition/ html/lexical.html#3.10.6.

2.4 Cómo mostrar texto con printf

Java SE 5.0 agregó el método System.out.printf para mostrar datos con formato; la f en el nombre printf representa la palabra “formato”. La figura 2.6 muestra las cadenas "Bienvenido a" y "la programacion en Java!" con System.out.printf. Las líneas 9 y 10 System.out.printf( "%s\n%s\n", "Bienvenido a", "la programacion en Java!" );

llaman al método System.out.printf para mostrar la salida del programa. La llamada al método especifica tres argumentos. Cuando un método requiere varios argumentos, éstos se separan con comas (,); a esto se le conoce como lista separada por comas.

Buena práctica de programación 2.9 Coloque un espacio después de cada coma (,) en una lista de argumentos, para que sus programas sean más legibles.

Recuerde que todas las instrucciones en Java terminan con un punto y coma (;). Por lo tanto, las líneas 9 y 10 sólo representan una instrucción. Java permite que las instrucciones largas se dividan en varias líneas. Sin embargo, no puede dividir una instrucción a la mitad de un identificador, o de una cadena.

Error común de programación 2.7 Dividir una instrucción a la mitad de un identificador o de una cadena es un error de sintaxis.

www.elsolucionario.net

44

1 2 3 4 5 6 7 8 9 10 11 12 13 14

Capítulo 2

Introducción a las aplicaciones en Java

// Fig. 2.6: Bienvenido4.java // Imprimir varias líneas en un cuadro de diálogo. public class Bienvenido4 { // el método main empieza la ejecución de la aplicación de Java public static void main( String args[] ) { System.out.printf( "%s\n%s\n", "Bienvenido a", "la programacion en Java!" ); } // fin del método main } // fin de la clase Bienvenido4

Bienvenido a la programacion en Java!

Figura 2.6 | Imprimir varias líneas de texto con el método System.out.printf. El primer argumento del método printf es una cadena de formato que puede consistir en texto fijo y especificadores de formato. El método printf imprime el texto fijo de igual forma que print o println. Cada especificador de formato es un receptáculo para un valor, y especifica el tipo de datos a imprimir. Los especificadores de formato también pueden incluir información de formato opcional. Los especificadores de formato empiezan con un signo porcentual (%) y van seguidos de un carácter que representa el tipo de datos. Por ejemplo, el especificador de formato %s es un receptáculo para una cadena. La cadena de formato en la línea 9 especifica que printf debe imprimir dos cadenas, y que a cada cadena le debe seguir un carácter de nueva línea. En la posición del primer especificador de formato, printf sustituye el valor del primer argumento después de la cadena de formato. En cada posición posterior de los especificadores de formato, printf sustituye el valor del siguiente argumento en la lista. Así, este ejemplo sustituye "Bienvenido a" por el primer %s y "la programacion en Java!" por el segundo %s. La salida muestra que se imprimieron dos líneas de texto. En nuestros ejemplos, presentaremos las diversas características de formato a medida que se vayan necesitando. El capítulo 29 presenta los detalles de cómo dar formato a la salida con printf.

2.5 Otra aplicación en Java: suma de enteros Nuestra siguiente aplicación lee (o recibe como entrada) dos enteros (números completos, como –22, 7, 0 y 1024) introducidos por el usuario mediante el teclado, calcula la suma de los valores y muestra el resultado. Este programa debe llevar la cuenta de los números que suministra el usuario para los cálculos que el programa realiza posteriormente. Los programas recuerdan números y otros datos en la memoria de la computadora, y acceden a esos datos a través de elementos del programa, conocidos como variables. El programa de la figura 2.7 demuestra estos conceptos. En los resultados de ejemplo, resaltamos las diferencias entre la entrada del usuario y la salida del programa.

1 2 3 4 5 6 7

// Fig. 2.7: Suma.java // Programa que muestra la suma de dos enteros. import java.util.Scanner; // el programa usa la clase Scanner public class Suma { // el método main empieza la ejecución de la aplicación en Java

Figura 2.7 | Programa que muestra la suma de dos enteros. (Parte 1 de 2).

www.elsolucionario.net

2.5

8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

Otra aplicación en Java: suma de enteros

45

public static void main( String args[] ) { // crea objeto Scanner para obtener la entrada de la ventana de comandos Scanner entrada = new Scanner( System.in ); int numero1; // primer número a sumar int numero2; // segundo número a sumar int suma; // suma de numero1 y numero2 System.out.print( "Escriba el primer entero: " ); // indicador numero1 = entrada.nextInt(); // lee el primer número del usuario System.out.print( "Escriba el segundo entero: " ); // indicador numero2 = entrada.nextInt(); // lee el segundo número del usuario suma = numero1 + numero2; // suma los números System.out.printf( "La suma es %d\n", suma ); // muestra la suma } // fin del método main } // fin de la clase Suma

Escriba el primer entero: 45 Escriba el segundo entero: 72 La suma es 117

Figura 2.7 | Programa que muestra la suma de dos enteros. (Parte 2 de 2).

Las líneas 1 y 2 // Fig. 2.7: Suma.java // Programa que muestra la suma de dos enteros.

indican el número de la figura, el nombre del archivo y el propósito del programa. La línea 3 import java.util.Scanner; // el programa usa la clase Scanner

es una declaración import que ayuda al compilador a localizar una clase que se utiliza en este programa. Una gran fortaleza de Java es su extenso conjunto de clases predefinidas que podemos utilizar, en vez de “reinventar la rueda”. Estas clases se agrupan en paquetes (colecciones con nombre de clases relacionadas) y se conocen en conjunto como la biblioteca de clases de Java, o Interfaz de Programación de Aplicaciones de Java (API de Java). Los programadores utilizan declaraciones import para identificar las clases predefinidas que se utilizan en un programa de Java. La declaración import en la línea 3 indica que este ejemplo utiliza la clase Scanner predefinida de Java (que veremos en breve) del paquete java.util. Después, el compilador trata de asegurarse que utilicemos la clase Scanner de manera apropiada.

Error común de programación 2.8 Todas las declaraciones import deben aparecer antes de la primera declaración de clase en el archivo. Colocar una declaración import dentro del cuerpo de la declaración de una clase, o después de la declaración de la misma, es un error de sintaxis.

Tip para prevenir errores 2.7 Por lo general, si olvida incluir una declaración import para una clase que utilice en su programa, se produce un error de compilación que contiene el mensaje: “cannot resolve symbol”. Cuando esto ocurra, verifique que haya proporcionado las declaraciones import apropiadas y que los nombres en las mismas estén escritos correctamente, incluyendo el uso apropiado de las letras mayúsculas y minúsculas.

www.elsolucionario.net

46

Capítulo 2

Introducción a las aplicaciones en Java

La línea 5 public class Suma

empieza la declaración de la clase Suma. El nombre de archivo para esta clase public debe ser Suma.java. Recuerde que el cuerpo de cada declaración de clase empieza con una llave izquierda de apertura (línea 6) ({) y termina con una llave derecha de cierre (línea 29) (}). La aplicación empieza a ejecutarse con el método main (líneas 8 a la 27). La llave izquierda (línea 9) marca el inicio del cuerpo de main, y la correspondiente llave derecha (línea 27) marca el final de main. Observe que al método main se le aplica un nivel de sangría en el cuerpo de la clase Suma, y que al código en el cuerpo de main se le aplica otro nivel para mejorar la legibilidad. La línea 11 Scanner entrada = new Scanner( System.in );

es una instrucción de declaración de variable (también conocida como declaración), la cual especifica el nombre (entrada) y tipo (Scanner) de una variable utilizada en este programa. Una variable es una ubicación en la memoria de la computadora, en donde se puede guardar un valor para utilizarlo posteriormente en un programa. Todas las variables deben declararse con un nombre y un tipo antes de poder usarse; este nombre permite al programa acceder al valor de la variable en memoria; y puede ser cualquier identificador válido. (Consulte en la sección 2.2 los requerimientos para nombrar identificadores). El tipo de una variable especifica el tipo de información que se guarda en esa ubicación de memoria. Al igual que las demás instrucciones, las instrucciones de declaración terminan con punto y coma (;). La declaración en la línea 11 especifica que la variable llamada entrada es de tipo Scanner. Un objeto Scanner permite a un programa leer datos (como números) para usarlos. Los datos pueden provenir de muchas fuentes, como un archivo en disco, o desde el teclado. Antes de usar un objeto Scanner, el programa debe crearlo y especificar el origen de los datos. El signo igual (=) en la línea 11 indica que la variable entrada tipo Scanner debe inicializarse (es decir, hay que prepararla para usarla en el programa) en su declaración con el resultado de la expresión new Scanner ( System.in ) a la derecha del signo igual. Esta expresión crea un objeto Scanner que lee los datos escritos por el usuario mediante el teclado. Recuerde que el objeto de salida estándar, System.out, permite a las aplicaciones de Java mostrar caracteres en la ventana de comandos. De manera similar, el objeto de entrada estándar, System.in, permite a las aplicaciones de Java leer la información escrita por el usuario. Así, la línea 11 crea un objeto Scanner que permite a la aplicación leer la información escrita por el usuario mediante el teclado. Las instrucciones de declaración de variables en las líneas 13 a la 15 int numero1; // primer número a sumar int numero2; // segundo número a sumar int suma; // suma de numero1 y numero2

declaran que las variables numero1, numero2 y suma contienen datos de tipo int; estas variables pueden contener valores enteros (números completos, como 7, –11, 0 y 31,914). Estas variables no se han inicializado todavía. El rango de valores para un int es de –2,147,483,648 a +2,147,483,647. Pronto hablaremos sobre los tipos float y double, para guardar números reales, y sobre el tipo char, para guardar datos de caracteres. Los números reales son números que contienen puntos decimales, como 3.4, 0.0 y –11.19. Las variables de tipo char representan caracteres individuales, como una letra en mayúscula (como A), un dígito (como 7), un carácter especial (como * o %) o una secuencia de escape (como el carácter de nueva línea, \n). Los tipos como int, float, double y char se conocen como tipos primitivos o tipos integrados. Los nombres de los tipos primitivos son palabras clave y, por lo tanto, deben aparecer completamente en minúsculas. El apéndice D, Tipos primitivos, sintetiza las características de los ocho tipos primitivos (boolean, byte, char, short, int, long, float y double). Las instrucciones de declaración de variables pueden dividirse en varias líneas, separando los nombres de las variables por comas (es decir, una lista de nombres de variables separados por comas). Varias variables del mismo tipo pueden declararse en una, o en varias declaraciones. Por ejemplo, las líneas 13 a la 15 se pueden escribir como una sola instrucción, de la siguiente manera: int numero1, // primer número a sumar numero2, // segundo número a sumar suma; // suma de numero1 y numero2

www.elsolucionario.net

2.5

Otra aplicación en Java: suma de enteros

47

Observe que utilizamos comentarios de fin de línea en las líneas 13 a la 15. Este uso de comentarios es una práctica común de programación, para indicar el propósito de cada variable en el programa.

Buena práctica de programación 2.10 Declare cada variable en una línea separada. Este formato permite insertar fácilmente un comentario descriptivo a continuación de cada declaración.

Buena práctica de programación 2.11 Seleccionar nombres de variables significativos ayuda a que un programa se autodocumente (es decir, que sea más fácil entender con sólo leerlo, en lugar de leer manuales o ver un número excesivo de comentarios).

Buena práctica de programación 2.12 Por convención, los identificadores de nombre de variables empiezan con una letra minúscula, y cada una de las palabras en el nombre, que van después de la primera, deben empezar con una letra mayúscula. Por ejemplo, el identificador primerNumero tiene una N mayúscula en su segunda palabra, Numero.

La línea 17 System.out.print( "Escriba el primer entero: " ); // indicador

utiliza System.out.print para mostrar el mensaje "Escriba el primer entero: ". Este mensaje se conoce como indicador, ya que indica al usuario que debe realizar una acción específica. En la sección 2.2 vimos que los identificadores que empiezan con letras mayúsculas representan nombres de clases. Por lo tanto, System es una clase; que forma parte del paquete java.lang. Observe que la clase System no se importa con una declaración import al principio del programa.

Observación de ingeniería de software 2.1 El paquete java.lang se importa de manera predeterminada en todos los programas de Java; por ende, las clases en java.lang son las únicas en la API que no requieren una declaración import.

La línea 18 numero1 = entrada.nextInt(); // lee el primer número del usuario

utiliza el método nextInt del objeto entrada de la clase Scanner para obtener un entero del usuario mediante el teclado. En este punto, el programa espera a que el usuario escriba el número y oprima Intro para enviar el número al programa. Técnicamente, el usuario puede escribir cualquier cosa como valor de entrada. Nuestro programa asume que el usuario escribirá un valor de entero válido, según las indicaciones; si el usuario escribe un valor no entero, se producirá un error lógico en tiempo de ejecución y el programa no funcionará correctamente. El capítulo 13, Manejo de excepciones, habla sobre cómo hacer sus programas más robustos, al permitirles manejar dichos errores. Esto también se conoce como hacer que su programa sea tolerante a fallas. En la línea 18, el resultado de la llamada al método nextInt (un valor int) se coloca en la variable numero1 mediante el uso del operador de asignación, =. La instrucción se lee como “numero1 obtiene el valor de entrada.nextInt()”. Al operador = se le llama operador binario, ya que tiene dos operandos: numero1 y el resultado de la llamada al método entrada.nextInt(). Esta instrucción se llama instrucción de asignación, ya que asigna un valor a una variable. Todo lo que está a la derecha del operador de asignación (=) se evalúa siempre antes de realizar la asignación.

Buena práctica de programación 2.13 Coloque espacios en cualquier lado de un operador binario, para que resalte y el programa sea más legible.

La línea 20 System.out.print( "Escriba el segundo entero: " ); // indicador

www.elsolucionario.net

48

Capítulo 2

Introducción a las aplicaciones en Java

pide al usuario que escriba el segundo entero. La línea 21 numero2 = entrada.nextInt(); // lee el segundo número del usuario

lee el segundo entero y lo asigna a la variable numero2. La línea 23 suma = numero1 + numero2; // suma los números

es una instrucción de asignación que calcula la suma de las variables numero1 y numero2, y asigna el resultado a la variable suma mediante el uso del operador de asignación, =. La instrucción se lee como “suma obtiene el valor de numero1 + numero2”. La mayoría de los cálculos se realizan en instrucciones de asignación. Cuando el programa encuentra la operación de suma, utiliza los valores almacenados en las variables numero1 y numero2 para realizar el cálculo. En la instrucción anterior, el operador de suma es binario; sus dos operandos son numero1 y numero2. Las partes de las instrucciones que contienen cálculos se llaman expresiones. De hecho, una expresión es cualquier parte de una instrucción que tiene un valor asociado. Por ejemplo, el valor de la expresión numero1 + numero2 es la suma de los números. De manera similar, el valor de la expresión entrada.nextInt() es un entero escrito por el usuario. Una vez realizado el cálculo, la línea 25 System.out.printf( "La suma es %d\n", suma ); // muestra la suma

utiliza el método System.out.printf para mostrar la suma. El especificador de formato %d es un receptáculo para un valor int (en este caso, el valor de suma); la letra d representa “entero decimal”. Observe que aparte del especificador de formato %d, el resto de los caracteres en la cadena de formato son texto fijo. Por lo tanto, el método printf imprime en pantalla "La suma es ", seguido del valor de suma (en la posición del especificador de formato %d) y una nueva línea. Observe que los cálculos también pueden realizarse dentro de instrucciones printf. Podríamos haber combinado las instrucciones de las líneas 23 y 25 en la siguiente instrucción: System.out.printf( "La suma es %d\n", ( numero1 + numero2 ) );

Los paréntesis alrededor de la expresión numero1 + numero2 no son requeridos; se incluyen para enfatizar que el valor de la expresión se imprime en la posición del especificador de formato %d.

Documentación de la API de Java Para cada nueva clase de la API de Java que utilizamos, indicamos el paquete en el que se ubica. Esta información es importante, ya que nos ayuda a localizar las descripciones de cada paquete y clase en la documentación de la API de Java. Puede encontrar una versión basada en Web de esta documentación en java.sun.com/javase/6/docs/api/

También puede descargar esta documentación, en su propia computadora, de java.sun.com/javase/downloads/ea.jsp

La descarga es de aproximadamente 53 megabytes (MB). El apéndice J, Uso de la documentación de la API de Java, describe cómo utilizar esta documentación.

2.6 Conceptos acerca de la memoria Los nombres de variables como numero1, numero2 y suma en realidad corresponden a ciertas ubicaciones en la memoria de la computadora. Toda variable tiene un nombre, un tipo, un tamaño y un valor. En el programa de suma de la figura 2.7, cuando se ejecuta la instrucción (línea 18) numero1 = entrada.nextInt(); // lee el primer número del usuario

el número escrito por el usuario se coloca en una ubicación de memoria a la cual se asigna el nombre numero1. Suponga que el usuario escribe 45. La computadora coloca ese valor entero en la ubicación numero1, como se

www.elsolucionario.net

2.7

numero1

Aritmética

49

45

Figura 2.8 | Ubicación de memoria que muestra el nombre y el valor de la variable numero1.

numero1

45

numero2

72

Figura 2.9 | Ubicaciones de memoria, después de almacenar valores para numero1 y numero2.

numero1

45

numero2

72

suma

117

Figura 2.10 | Ubicaciones de memoria, después de almacenar la suma de numero1 y numero2. muestra en la figura 2.8. Cada vez que se coloca un nuevo valor en una ubicación de memoria, se sustituye al valor anterior en esa ubicación; es decir, el valor anterior se pierde. Cuando se ejecuta la instrucción (línea 21) numero2 = entrada.nextInt(); // lee el segundo número del usuario

suponga que el usuario escribe 72. La computadora coloca ese valor entero en la ubicación numero2. La memoria ahora aparece como se muestra en la figura 2.9. Una vez que el programa de la figura 2.7 obtiene valores para numero1 y numero2, los suma y coloca el resultado en la variable suma. La instrucción (línea 23) suma = numero1 + numero2; // suma los números

realiza la suma y después sustituye el valor anterior de suma. Una vez que se calcula suma, la memoria aparece como se muestra en la figura 2.10. Observe que los valores de numero1 y numero2 aparecen exactamente como antes de usarlos en el cálculo de suma. Estos valores se utilizaron, pero no se destruyeron, cuando la computadora realizó el cálculo. Por ende, cuando se lee un valor de una ubicación de memoria, el proceso es no destructivo.

2.7 Aritmética La mayoría de los programas realizan cálculos aritméticos. Los operadores aritméticos se sintetizan en la figura 2.11. Observe el uso de varios símbolos especiales que no se utilizan en álgebra. El asterisco (*) indica la multiplicación, y el signo de porcentaje (%) es el operador residuo (conocido como módulo en algunos lenguajes), el cual describiremos en breve. Los operadores aritméticos en la figura 2.11 son binarios, ya que funcionan con dos operandos. Por ejemplo, la expresión f + 7 contiene el operador binario + y los dos operandos f y 7. La división de enteros produce un cociente entero: por ejemplo, la expresión 7 / 4 da como resultado 1, y la expresión 17 / 5 da como resultado 3. Cualquier parte fraccionaria en una división de enteros simplemente se descarta (es decir, se trunca); no ocurre un redondeo. Java proporciona el operador residuo, %, el cual produce el residuo después de la división. La expresión x % y produce el residuo después de que x se divide entre y. Por lo tanto, 7 % 4 produce 3, y 17 % 5 produce 2. Por lo general, este operador se utiliza más con operandos enteros, pero también puede usarse con otros tipos aritméticos. En los ejercicios de este capítulo y de capítulos posteriores, consideramos muchas aplicaciones interesantes del operador residuo, como determinar si un número es múltiplo de otro.

www.elsolucionario.net

50

Capítulo 2

Introducción a las aplicaciones en Java

Operación en Java

Operador aritmético

Expresión algebraica

Expresión en Java

Suma

+

f+7

f + 7

Resta

-

p—c

p — c

Multiplicación

*

bm

b * m

División

/

x / y o x-y o x ∏ y

x / y

Residuo

%

r mod s

r % s

Figura 2.11 | Operadores aritméticos.

Expresiones aritméticas en formato de línea recta Las expresiones aritméticas en Java deben escribirse en formato de línea recta para facilitar la escritura de programas en la computadora. Por lo tanto, las expresiones como “a dividida entre b” deben escribirse como a / b, de manera que todas las constantes, variables y operadores aparezcan en una línea recta. La siguiente notación algebraica no es generalmente aceptable para los compiladores: a b

Paréntesis para agrupar subexpresiones Los paréntesis se utilizan para agrupar términos en las expresiones en Java, de la misma manera que en las expresiones algebraicas. Por ejemplo, para multiplicar a por la cantidad b + c, escribimos a * ( b + c )

Si una expresión contiene paréntesis anidados, como ( ( a + b ) * c )

se evalúa primero la expresión en el conjunto más interno de paréntesis (a

+ b

en este caso).

Reglas de precedencia de operadores Java aplica los operadores en expresiones aritméticas en una secuencia precisa, determinada por las siguientes reglas de precedencia de operadores, que generalmente son las mismas que las que se utilizan en álgebra (figura 2.12): 1. Las operaciones de multiplicación, división y residuo se aplican primero. Si una expresión contiene varias de esas operaciones, los operadores se aplican de izquierda a derecha. Los operadores de multiplicación, división y residuo tienen el mismo nivel de precedencia. 2. Las operaciones de suma y resta se aplican a continuación. Si una expresión contiene varias de esas operaciones, los operadores se aplican de izquierda a derecha. Los operadores de suma y resta tienen el mismo nivel de precedencia.

Operador(es)

Operación(es)

Orden de evaluación (precedencia)

*

Multiplicación División Residuo

Se evalúan primero. Si hay varios operadores de este tipo, se evalúan de izquierda a derecha.

Suma Resta

Se evalúan después. Si hay varios operadores de este tipo, se evalúan de izquierda a derecha.

/ % + -

Figura 2.12 | Precedencia de los operadores aritméticos.

www.elsolucionario.net

2.7

Aritmética

51

Estas reglas permiten a Java aplicar los operadores en el orden correcto. Cuando decimos que los operadores se aplican de izquierda a derecha, nos referimos a su asociatividad; veremos que algunos se asocian de derecha a izquierda. La figura 2.12 sintetiza estas reglas de precedencia de operadores; esta tabla se expandirá a medida que se introduzcan más operadores en Java. En el apéndice A, Tabla de precedencia de los operadores, se incluye una tabla de precedencias completa.

Ejemplos de expresiones algebraicas y de Java Ahora, consideremos varias expresiones en vista de las reglas de precedencia de operadores. Cada ejemplo enlista una expresión algebraica y su equivalente en Java. El siguiente es un ejemplo de una media (promedio) aritmética de cinco términos: Álgebra:

m= a+b+c+d+e 5

Java:

m = ( a + b + c + d + e ) / 5;

Los paréntesis son obligatorios, ya que la división tiene una mayor precedencia que la suma. La cantidad completa ( a + b + c + d + e ) va a dividirse entre 5. Si por error se omiten los paréntesis, obtenemos a + b + c + d + e / 5, lo cual da como resultado a+b+c+d+e 5 El siguiente es un ejemplo de una ecuación de línea recta: Álgebra:

y = mx + b

Java:

y = m * x + b;

No se requieren paréntesis. El operador de multiplicación se aplica primero, ya que la multiplicación tiene mayor precedencia sobre la suma. La asignación ocurre al último, ya que tiene menor precedencia que la multiplicación o suma. El siguiente ejemplo contiene las operaciones residuo (%), multiplicación, división, suma y resta: Álgebra:

z = pr%q + w/x – y

Java:

z

=

p

6

*

r

1

%

q

2

+

w

/

4

3

x



y;

5

Los números dentro de los círculos bajo la instrucción, indican el orden en el que Java aplica los operadores. Las operaciones de multiplicación, residuo y división se evalúan primero, en orden de izquierda a derecha (es decir, se asocian de izquierda a derecha), ya que tienen mayor precedencia que la suma y la resta. Las operaciones de suma y resta se evalúan a continuación; estas operaciones también se aplican de izquierda a derecha.

Evaluación de un polinomio de segundo grado Para desarrollar una mejor comprensión de las reglas de precedencia de operadores, considere la evaluación de un polinomio de segundo grado (y = ax2 + bx + c): y = 6

a

* 1

x

* 2

x

+ 4

b

* 3

x

+

c;

5

Los números dentro de los círculos indican el orden en el que Java aplica los operadores. Las operaciones de multiplicación se evalúan primero en orden de izquierda a derecha (es decir, se asocian de izquierda a derecha), ya que tienen mayor precedencia que la suma. Las operaciones de suma se evalúan a continuación y se aplican de izquierda a derecha. No existe un operador aritmético para la potencia de un número en Java, por lo que x 2 se representa como x * x. La sección 5.4 muestra una alternativa para calcular la potencia de un número en Java.

www.elsolucionario.net

52

Capítulo 2

Introducción a las aplicaciones en Java

Paso 1.

y = 2 * 5 * 5 + 3 * 5 + 7;

(Multiplicación de más a la izquierda)

2 * 5 es 10

Paso 2.

y = 10 * 5 + 3 * 5 + 7;

(Multiplicación de más a la izquierda)

10 * 5 es 50

Paso 3.

y = 50 + 3 * 5 + 7;

(Multiplicación antes de la suma)

3 * 5 es 15

Paso 4.

y = 50 + 15 + 7;

(Suma de más a la izquierda)

50 + 15 es 65

Paso 5.

y = 65 + 7;

(Última suma)

65 + 7 es 72

Paso 6.

(Última operación; colocar 72 en y)

y = 72

Figura 2.13 | Orden en el cual se evalúa un polinomio de segundo grado.

Suponga que a, b, c y x en el polinomio de segundo grado anterior se inicializan (reciben valores) como sigue: a = 2, b = 3, c = 7 y x = 5. La figura 2.13 muestra el orden en el que se aplican los operadores. Al igual que en álgebra, es aceptable colocar paréntesis innecesarios en una expresión para hacer que ésta sea más clara. A dichos paréntesis se les llama paréntesis redundantes. Por ejemplo, la instrucción de asignación anterior podría colocarse entre paréntesis, de la siguiente manera: y = ( a * x * x ) + ( b * x ) + c;

Buena práctica de programación 2.14 El uso de paréntesis para las expresiones aritméticas complejas, incluso cuando éstos no sean necesarios, puede hacer que las expresiones aritméticas sean más fáciles de leer.

2.8 Toma de decisiones: operadores de igualdad y relacionales Una condición es una expresión que puede ser verdadera (true) o falsa (false). Esta sección presenta la instrucción if de Java, la cual permite que un programa tome una decisión, con base en el valor de una condición. Por ejemplo, la condición “calificación es mayor o igual que 60” determina si un estudiante pasó o no una prueba. Si la condición en una instrucción if es verdadera, el cuerpo de la instrucción if se ejecuta. Si la condición es falsa, el cuerpo no se ejecuta. Veremos un ejemplo en breve. Las condiciones en las instrucciones if pueden formarse utilizando los operadores de igualdad (== y !=) y los operadores relacionales (>, = y

>

x > y

x

es mayor que

y

<

<

x < y

x

es menor que

y



>=

x >= y

x

es mayor o igual que

y



%d\n”, numero1, numero2 ); if ( numero1 = %d\n”, numero1, numero2 ); } // fin del método main } // fin de la clase Comparacion

Escriba el primer entero: 777 Escriba el segundo entero: 777 777 == 777 777 = 777

Escriba el primer entero: 1000 Escriba el segundo entero: 2000 1000 != 2000 1000 < 2000 1000 1000 2000 >= 1000

Figura 2.15 | Operadores de igualdad y relacionales. (Parte 2 de 2).

La declaración de la clase Comparacion comienza en la línea 6 public class Comparacion

El método main de la clase (líneas 9 a 41) empieza la ejecución del programa. La línea 12 Scanner entrada = new Scanner( System.in );

declara la variable entrada de la clase estándar (es decir, el teclado). Las líneas 14 y 15

Scanner

y le asigna un objeto

Scanner

que recibe datos de la entrada

int numero1; // primer número a comparar int numero2; // segundo número a comparar

declaran las variables int que se utilizan para almacenar los valores introducidos por el usuario. Las líneas 17-18 System.out.print( "Escriba el primer entero: " ); // indicador numero1 = entrada.nextInt(); // lee el primer número del usuario

piden al usuario que introduzca el primer entero y el valor, respectivamente. El valor de entrada se almacena en la variable numero1. Las líneas 20-21 System.out.print( "Escriba el segundo entero: " ); // indicador numero2 = entrada.nextInt(); // lee el segundo número del usuario

piden al usuario que introduzca el segundo entero y el valor, respectivamente. El valor de entrada se almacena en la variable numero2.

www.elsolucionario.net

2.8

Toma de decisiones: operadores de igualdad y relacionales

55

Las líneas 23-24 if ( numero1 == numero2 ) System.out.printf( “%d == %d\n”, numero1, numero2 );

declaran una instrucción if que compara los valores de las variables numero1 y numero2, para determinar si son iguales o no. Una instrucción if siempre empieza con la palabra clave if, seguida de una condición entre paréntesis. Una instrucción if espera una instrucción en su cuerpo. La sangría de la instrucción del cuerpo que se muestra aquí no es obligatoria, pero mejora la legibilidad del programa al enfatizar que la instrucción en la línea 24 forma parte de la instrucción if que empieza en la línea 23. La línea 24 sólo se ejecuta si los números almacenados en las variables numero1 y numero2 son iguales (es decir, si la condición es verdadera). Las instrucciones if en las líneas 26-27, 29-30, 32-33, 35-36 y 38-39 comparan a numero1 y numero2 con los operadores !=, , =, respectivamente. Si la condición en cualquiera de las instrucciones if es verdadera, se ejecuta la instrucción del cuerpo correspondiente.

Error común de programación 2.9 Olvidar los paréntesis izquierdo y/o derecho de la condición en una instrucción if es un error de sintaxis; los paréntesis son obligatorios.

Error común de programación 2.10 Confundir el operador de igualdad (==) con el de asignación (=) puede producir un error lógico o de sintaxis. El operador de igualdad debe leerse como “es igual a”, y el de asignación como “obtiene” u “obtiene el valor de”. Para evitar confusión, algunas personas leen el operador de igualdad como “doble igual” o “igual igual”.

Error común de programación 2.11 Es un error de sintaxis si los operadores ==, !=, >= y = y < =, respectivamente.

Error común de programación 2.12 Invertir los operadores !=, >= y y ==

Asociatividad

Tipo

izquierda a derecha

multiplicativa

izquierda a derecha

suma

izquierda a derecha

relacional

izquierda a derecha

igualdad

derecha a izquierda

asignación

Figura 2.16 | Precedencia y asociatividad de los operadores descritos hasta ahora. ejecuta, sin importar que la condición sea verdadera o falsa, ya que la instrucción de salida no forma parte de la instrucción if.

Error común de programación 2.13 Colocar un punto y coma inmediatamente después del paréntesis derecho de la condición en una instrucción if es, generalmente, un error lógico.

Observe el uso del espacio en blanco en la figura 2.15. Recuerde que los caracteres de espacio en blanco, como tabuladores, nuevas líneas y espacios, generalmente son ignorados por el compilador. Por lo tanto, las instrucciones pueden dividirse en varias líneas y pueden espaciarse de acuerdo a las preferencias del programador, sin afectar el significado de un programa. Es incorrecto dividir identificadores y cadenas. Idealmente las instrucciones deben mantenerse lo más reducidas que sea posible, pero no siempre se puede hacer esto.

Buena práctica de programación 2.17 Una instrucción larga puede esparcirse en varias líneas. Si una sola instrucción debe dividirse entre varias líneas, los puntos que elija para hacer la división deben tener sentido, como después de una coma en una lista separada por comas, o después de un operador en una expresión larga. Si una instrucción se divide entre dos o más líneas, aplique sangría a todas las líneas subsecuentes hasta el final de la instrucción.

La figura 2.16 muestra la precedencia de los operadores que se presentan en este capítulo. Los operadores se muestran de arriba hacia abajo, en orden descendente de precedencia; todos, con la excepción del operador de asignación, =, se asocian de izquierda a derecha. La suma es asociativa a la izquierda, por lo que una expresión como x + y + z se evalúa como si se hubiera escrito así: ( x + y ) + z. El operador de asignación, =, asocia de derecha a izquierda, por lo que una expresión como x = y = 0 se evalúa como si se hubiera escrito así: x = ( y = 0 ), en donde, como pronto veremos, primero se asigna el valor 0 a la variable y, y después se asigna el resultado de esa asignación, 0, a x.

Buena práctica de programación 2.18 Cuando escriba expresiones que contengan muchos operadores, consulte la tabla de precedencia (apéndice A). Confirme que las operaciones en la expresión se realicen en el orden que usted espera. Si no está seguro acerca del orden de evaluación en una expresión compleja, utilice paréntesis para forzar el orden, en la misma forma que lo haría con las expresiones algebraicas. Observe que algunos operadores como el de asignación, =, asocian de derecha a izquierda, en vez de hacerlo de izquierda a derecha.

2.9 (Opcional) Ejemplo práctico de Ingeniería de Software: cómo examinar el documento de requerimientos de un problema Ahora empezaremos nuestro ejemplo práctico opcional de diseño e implementación orientados a objetos. Las secciones del Ejemplo práctico de Ingeniería de Software al final de este y los siguientes capítulos le ayudarán a incursionar en la orientación a objetos, mediante el análisis de un ejemplo práctico de una máquina de cajero automático

www.elsolucionario.net

2.9

(Opcional) Ejemplo práctico de Ingeniería de Software: cómo examinar el documento de ...

57

(Automated Teller Machine o ATM, por sus siglas en inglés). Este ejemplo práctico le brindará una experiencia de diseño e implementación substancial, cuidadosamente pautada y completa. En los capítulos 3 al 8 y 10, llevaremos a cabo los diversos pasos de un proceso de diseño orientado a objetos (DOO) utilizando UML, mientras relacionamos estos pasos con los conceptos orientados a objetos que se describen en los capítulos. El apéndice M implementa el ATM utilizando las técnicas de la programación orientada a objetos (POO) en Java. Presentaremos la solución completa al ejemplo práctico. É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 ejemplo práctico le ayudará a acostumbrarse a los tipos de problemas substanciales que se encuentran en la industria, y sus soluciones potenciales. Esperamos que disfrute esta experiencia de aprendizaje. Empezaremos nuestro proceso de diseño con la presentación de un documento de requerimientos, el cual especifica el propósito general del sistema ATM y qué debe hacer. A lo largo del ejemplo práctico, nos referiremos al documento de requerimientos para determinar con precisión la funcionalidad que debe incluir el sistema.

Documento de requerimientos Un 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 2.17). 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. El dispensador de efectivo comienza cada día cargado con 500 billetes de $20. [Nota: debido al alcance limitado de este ejemplo práctico, ciertos elementos del ATM que se describen aquí no imitan exactamente a los de un ATM real. Por ejemplo, generalmente un ATM contiene un dispositivo que lee el número de cuenta del usuario de una tarjeta para ATM, mientras que este ATM 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].

Bienvenido! Escriba su número de cuenta: 12345 Pantalla Escriba su NIP: 54321

Tome aquí el efectivo

Dispensador de efectivo

Teclado Inserte aquí el sobre de depósito

Figura 2.17 | Interfaz de usuario del cajero automático.

www.elsolucionario.net

Ranura de depósito

58

Capítulo 2

Introducción a las aplicaciones en Java

El banco desea que usted desarrolle software para realizar las transacciones financieras que inicien los clientes a través del ATM. Posteriormente, 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 involucrado en la manera en que estos dispositivos ejecutan su tarea. El hardware del ATM no se ha desarrollado aún, 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 cuentas del banco (es decir, una colección organizada de datos almacenados en una computadora). 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 para que varios ATMs puedan acceder a esta base de datos al mismo tiempo. Lo que es más, supongamos 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 ATM se topa con cuestiones de seguridad con una complejidad razonable, 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 (suponiendo que nadie lo está utilizando), el usuario deberá experimentar la siguiente secuencia de eventos (vea la figura 2.17): 1. La pantalla muestra un mensaje de bienvenida 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 uso del 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. 5. Si el usuario introduce un número de cuenta válido y el NIP correcto para esa cuenta, la pantalla muestra el menú principal (figura 2.18). 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. Una vez que el ATM autentica al usuario, el menú principal (figura 2.18) 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) 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 2.19) 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). 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 muestra un mensaje indicando esta situación y pide al usuario que seleccione un monto más pequeñ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.

www.elsolucionario.net

2.9

(Opcional) Ejemplo práctico de Ingeniería de Software: cómo examinar el documento de ...

59

Menú principal 1 - Ver mi saldo 2 - Retirar efectivo 3 - Depositar fondos 4 - Salir Escriba una opción:

Tome aquí el efectivo

Inserte aquí el sobre de depósito

Figura 2.18 | Menú principal del ATM.

Menú de retiro 1 - $20 4 - $100 2 - $40 5 - $200 3 - $60 6 - Cancelar transacción Elija un monto de retiro:

Tome aquí el efectivo

Inserte aquí el sobre de depósito

Figura 2.19 | Menú de retiro del ATM.

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 usuario 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.

www.elsolucionario.net

60

Capítulo 2

Introducción a las aplicaciones en Java

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 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 teclado 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, $1.25), sino que debe escribir un monto de depósito en forma de número de centavos (por ejemplo, 125). Después, el ATM divide este número entre 100 para obtener un número que represente un monto en dólares (por ejemplo, 125 ÷ 100 = 1.25)]. 3. Si el usuario especifica un monto a depositar, el ATM procede al paso 4. Si elije cancelar la transacció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 2 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 dinero 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 validarse (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 independiente 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 siguiente usuario.

Análisis del sistema de ATM En 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 requerimientos, 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 documento 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 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 del ciclo de vida del software 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 problema correcto. Los analistas de sistemas recolectan los requerimientos que indican el problema especí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.

www.elsolucionario.net

2.9

(Opcional) Ejemplo práctico de Ingeniería de Software: cómo examinar el documento de ...

61

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 ATMs 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 ejemplo práctico 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 requerimientos; 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-uso Ahora presentaremos el primero de varios diagramas de UML en el ejemplo práctico. Crearemos un diagrama de caso-uso para modelar las interacciones entre los clientes de un sistema (en este ejemplo práctico, 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 ejemplo práctico). 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 aparece 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 herramientas indispensables que ayudan a los diseñadores de sistemas a enfocarse en satisfacer las necesidades de los usuarios. La figura 2.20 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 persona u otro sistema) cuando interactúa con el sistema. Para nuestro cajero automático, el actor es un Usuario que puede ver el saldo de una cuenta, retirar efectivo y depositar fondos del ATM. El Usuario 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. Hay que tener en cuenta que 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) desempeña el papel del usuario para realizar transacciones financieras. La figura 2.20 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 programadores 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)

Ver saldo de cuenta

Retirar efectivo

Usuario Depositar fondos

Figura 2.20 | Diagrama de caso-uso para el sistema ATM, desde la perspectiva del usuario.

www.elsolucionario.net

62

Capítulo 2

Introducción a las aplicaciones en Java

debe detallar claramente cómo debe construirse el sistema para satisfacer estos requerimientos. En las siguientes secciones del Ejemplo práctico de Ingeniería de Software, 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 procesos, de los cuales el más conocido es Rational Unified Process™ (RUP), desarrollado por Rational Software Corporation. RUP es un proceso robusto para diseñar aplicaciones a nivel industrial. Para este ejemplo práctico, 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 ATM Ahora 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 designadas, nuestro sistema ATM tiene una interfaz de usuario (figura 2.17), 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 comportamiento 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 ambos. Existen diversos tipos de estructuras y comportamientos de un sistema. Por ejemplo, las interacciones 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 ejemplo práctico, uno de los cuales (el diagrama de clases) modela la estructura del sistema, mientras que los otros cinco modelan el comportamiento. En el apéndice O, UML 2: Tipos de diagramas adicionales, veremos las generalidades sobre los siete tipos restantes de diagramas de UML. 1. Los diagramas de caso-uso, como el de la figura 2.20, modelan las interacciones entre un sistema y sus entidades externas (actores) en términos de casos de uso (capacidades del sistema, como “Ver saldo de cuenta”, “Retirar efectivo” y “Depositar fondos”). 2. Los diagramas de clases, que estudiará en la sección 3.10, 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 efectivo y una ranura para depósitos. 3. Los diagramas de máquina de estado, que estudiará en la sección 5.11, 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, puede 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 5.11, modelan la actividad de un objeto: el flujo de trabajo (secuencia de eventos) del objeto 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 cuentas 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 anteriores de UML) modelan las interacciones entre los objetos en un sistema, con un énfasis acerca de qué interacciones ocurren. En la sección 7.14 aprenderá que estos diagramas muestran 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 obtener el saldo de una cuenta.

www.elsolucionario.net

2.9

(Opcional) Ejemplo práctico de Ingeniería de Software: cómo examinar el documento de ...

63

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 interacciones. En la sección 7.14 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 3.10 seguiremos diseñando nuestro sistema ATM; ahí identificaremos las clases del documento de requerimientos. Para lograr esto, extraeremos sustantivos clave y frases nominales del documento de requerimientos. Mediante el uso de estas clases, desarrollaremos nuestro primer borrador del diagrama de clases que modelará la estructura de nuestro sistema ATM.

Recursos en Internet y Web Los siguientes URLs proporcionan información sobre el diseño orientado a objetos con UML. www-306.ibm.com/software/rational/uml/

Lista preguntas frecuentes acerca del UML, proporcionado por IBM Rational. www.douglass.co.uk/documents/softdocwiz.com.UML.htm

Sitio anfitrión del Diccionario del Lenguaje unificado de modelado, el cual lista y define todos los términos utilizados en el UML. www-306.ibm.com/software/rational/offerings/design.html

Proporciona información acerca del software IBM Rational, disponible para el diseño de sistemas. Ofrece descargas de versiones de prueba de 30 días de varios productos, como IBM Rational Rose® XDE Developer. www.embarcadero.com/products/describe/index.html

Proporciona una licencia gratuita de 14 días para descargar una versión de prueba de Describe™: una herramienta de modelado con UML de Embarcadero Technologies®. www.borland.com/us/products/together/index.html

Proporciona una licencia gratuita de 30 días para descargar una versión de prueba de Borland® Together® Control Center™: una herramienta de desarrollo de software que soporta el UML. www.ilogix.com/sublevel.aspx?id=53 http://modelingcommunity.telelogic.com/developer-trial.aspx

Proporciona una licencia gratuita de 30 días para descargar una versión de prueba de I-Logix Rhapsody®: un entorno de desarrollo controlado por modelos y basado en UML 2. argouml.tigris.org

Contiene información y descargas para ArgoUML, una herramienta gratuita de código fuente abierto de UML, escrita en Java. www.objectsbydesign.com/books/booklist.html

Provee una lista de libros acerca de UML y el diseño orientado a objetos. www.objectsbydesign.com/tools/umltools_byCompany.html

Provee una lista de herramientas de software que utilizan UML, como IBM Rational Rose, Embarcadero Describe, Sparx Systems Enterprise Architect, I-Logix Rhapsody y Gentleware Poseidon para UML. www.ootips.org/ood-principles.html

Proporciona respuestas a la pregunta “¿Qué se requiere para tener un buen diseño orientado a objetos?” parlezuml.com/tutorials/umlforjava.htm

Ofrece un tutorial de UML para desarrolladores de Java, el cual presenta los diagramas de UML y los compara detalladamente con el código que los implementa. www.cetus-links.org/oo_uml.html

Introduce el UML y proporciona vínculos a numerosos recursos sobre UML. www.agilemodeling.com/essays/umlDiagrams.htm

Proporciona descripciones detalladas y tutoriales acerca de cada uno de los 13 tipos de diagramas de UML 2.

Lecturas recomendadas Los siguientes libros proporcionan información acerca del diseño orientado a objetos con UML. Booch, G. Object-Oriented Analysis and Design with Applications, Tercera edición. Boston: Addison-Wesley, 2004. Eriksson, H. et al. UML 2 Toolkit. Nueva York: John Wiley, 2003. Kruchten, P. The Rational Unified Process: An Introduction. Boston: Addison-Wesley, 2004.

www.elsolucionario.net

64

Capítulo 2

Introducción a las aplicaciones en Java

Larman, C. Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design, Segunda edición. Upper Saddle River, NJ: Prentice Hall, 2002. Roques, P. UML in Practice: The Art of Modeling Software Systems Demonstrated Through Worked Examples and Solutions. Nueva York: John Wiley, 2004. Rosenberg, D. y K. Scott. Applying Use Case Driven Object Modeling with UML: An Annotated e-Commerce Example. Reading, MA: Addison-Wesley, 2001. Rumbaugh, J., I. Jacobson y G. Booch. The Complete UML Training Course. Upper Saddle River, NJ: Prentice Hall, 2000. Rumbaugh, J., I. Jacobson y G. Booch. The Unified Modeling Language Reference Manual. Reading, MA: AddisonWesley, 1999. Rumbaugh, J., I. Jacobson y G. Booch. The Unified Software Development Process. Reading, MA: Addison-Wesley, 1999.

Ejercicios de autoevaluación del Ejemplo práctico de Ingeniería de Software 2.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 2.20 para reflejar este cambio. 2.2 Los modelan las interacciones entre los objetos en un sistema, con énfasis acerca de cuándo ocurren estas interacciones. a) Diagramas de clases b) Diagramas de secuencia c) Diagramas de comunicación d) Diagramas de actividad 2.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, prueba b) diseño, análisis, prueba, implementación c) análisis, diseño, prueba, implementación d) análisis, diseño, implementación, prueba

Respuestas a los ejercicios de autoevaluación del Ejemplo práctico de Ingeniería de Software 2.1 La figura 2.21 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. 2.2

b.

2.3

d.

Ver saldo de cuenta

Retirar efectivo

Depositar fondos Usuario Transferir fondos entre cuentas

Figura 2.21 | 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.

www.elsolucionario.net

Resumen

65

2.10 Conclusión En este capítulo aprendió muchas características importantes de Java, incluyendo cómo mostrar datos en la pantalla en un Símbolo del sistema, recibir datos del teclado, realizar cálculos y tomar decisiones. Las aplicaciones que presentamos aquí le sirvieron como una introducción a los conceptos básicos de programación. Como verá en el capítulo 3, por lo general las aplicaciones de Java contienen sólo unas cuantas líneas de código en el método main; comúnmente estas instrucciones crean los objetos que realizan el trabajo de la aplicación. En el capítulo 3 aprenderá a implementar sus propias clases, y a utilizar objetos de esas clases en las aplicaciones.

Resumen Sección 2.2 Su primer programa en Java: imprimir una línea de texto • Los programadores de computadoras crean aplicaciones, escribiendo programas de cómputo. Una aplicación de Java es un programa de computadora que se ejecuta cuando utilizamos el comando java para iniciar la JVM. • Los programadores insertan comentarios para documentar los programas y mejorar su legibilidad. El compilador de Java ignora los comentarios. • Un comentario que empieza con // se llama comentario de fin de línea (o de una sola línea), ya que termina al final de la línea en la que aparece. • Los comentarios tradicionales (de varias líneas) pueden dividirse en varias líneas, y están delimitados por /* y */. El compilador ignora todo el texto entre los delimitadores. • Los comentarios Javadoc se delimitan por /** y */. Estos comentarios permiten a los programadores incrustar la documentación directamente en sus programas. La herramienta javadoc genera documentación en HTML, con base en los comentarios Javadoc. • La sintaxis de un lenguaje de programación especifica las reglas para crear un programa apropiado en ese lenguaje. • Un error de sintaxis (también conocido como error de compilador, error en tiempo de compilación o error de compilación) ocurre cuando el compilador encuentra código que viola las reglas del lenguaje Java. • Los programadores utilizan líneas en blanco y espacios para facilitar la lectura de los programas. En conjunto, las líneas en blanco, los espacios y los tabuladores se conocen como espacio en blanco. Los espacios y los tabuladores se conocen específicamente como caracteres de espacio en blanco. El compilador ignora el espacio en blanco. • Todo programa en Java consiste en por lo menos una declaración de clase, definida por el programador (también conocida como clase definida por el programador, o clase definida por el usuario). • Las palabras clave están reservadas para el uso exclusivo de Java, y siempre se escriben con letras minúsculas. • La palabra clave class introduce una declaración de clase, y va seguida inmediatamente del nombre de la clase. • Por convención, todos los nombres de las clases en Java empiezan con una letra mayúscula, y la primera letra de cada palabra subsiguiente también se escribe en mayúscula (como NombreClaseDeEjemplo). • El nombre de una clase de Java es un identificador: una serie de caracteres formada por letras, dígitos, guiones bajos ( _ ) y signos de dólar ($), que no empieza con un dígito y no contiene espacios. Por lo general, un identificador que no empieza con letra mayúscula no es el nombre de una clase de Java. • Java es sensible a mayúsculas/minúsculas; es decir, las letras mayúsculas y minúsculas son distintas. • El cuerpo de todas las declaraciones de clases debe estar delimitado por llaves, { y }. • La declaración de una clase public debe guardarse en un archivo con el mismo nombre que la clase, seguido de la extensión de nombre de archivo “.java”. • El método main es el punto de inicio de toda aplicación en Java, y debe empezar con: public static void main( String args[] ) en caso contrario, la JVM no ejecutará la aplicación. • Los métodos pueden realizar tareas y devolver información cuando completan éstas tareas. La palabra clave void indica que un método realizará una tarea, pero no devolverá información. • Las instrucciones instruyen a la computadora para que realice acciones. • Una secuencia de caracteres entre comillas dobles se llama cadena, cadena de caracteres, mensaje o literal de cadena. • System.out es el objeto de salida estándar; permite a las aplicaciones de Java mostrar caracteres en la ventana de comandos. • El método System.out.println muestra su argumento en la ventana de comandos, seguido de un carácter de nueva línea para colocar el cursor de salida en el inicio de la siguiente línea.

www.elsolucionario.net

66

Capítulo 2

Introducción a las aplicaciones en Java

• Toda instrucción termina con un punto y coma. • La mayoría de los sistemas operativos utilizan el comando cd para cambiar directorios en la ventana de comandos. • Para compilar un programa se utiliza el comando javac. Si el programa no contiene errores de sintaxis, se crea un archivo de clase que contiene los códigos de bytes de Java, los cuales representan a la aplicación. La JVM interpreta estos códigos de bytes cuando ejecutamos el programa.

Sección 2.3 Modificación de nuestro primer programa en Java •

muestra su argumento en pantalla y coloca el cursor de salida justo después del último carácter visualizado. • Una barra diagonal inversa (\) en una cadena es un carácter de escape. Indica que se va a imprimir un “carácter especial”. Java combina el siguiente carácter con la barra diagonal inversa para formar una secuencia de escape. La secuencia de escape \n representa el carácter de nueva línea, el cual coloca el cursor en la siguiente línea. System.out.print

Sección 2.4 Cómo mostrar texto con printf • El método System.out.printf (f significa “formato”) muestra datos con formato. • Cuando un método requiere varios argumentos, éstos se separan con comas (,); a esto se le conoce como lista separada por comas. • El primer argumento del método printf es una cadena de formato, que puede consistir en texto fijo y especificadores de formato. El método printf imprime el texto fijo de igual forma que print o println. Cada especificador de formato es un receptáculo para un valor, y especifica el tipo de datos a imprimir. • Los especificadores de formato empiezan con un signo porcentual (%), y van seguidos de un carácter que representa el tipo de datos. El especificador de formato %s es un receptáculo para una cadena. • En la posición del primer especificador de formato, printf sustituye el valor del primer argumento después de la cadena de formato. En la posición de los siguientes especificadores de formato, printf sustituye el valor del siguiente argumento en la lista de argumentos.

Sección 2.5 Otra aplicación en Java: suma de enteros • Los enteros son números completos, como –22 ,7, 0 y 1024. • Una declaración import ayuda al compilador a localizar una clase que se utiliza en un programa. • Java cuenta con un extenso conjunto de clases predefinidas que los programadores pueden reutilizar, en vez de tener que “reinventar la rueda”. Estas clases se agrupan en paquetes: llamados colecciones de clases. • En conjunto, a los paquetes de Java se les conoce como la biblioteca de clases de Java, o la Interfaz de Programación de Aplicaciones de Java (API de Java). • Una instrucción de declaración de variable especifica el nombre y el tipo de una variable. • Una variable es una ubicación en la memoria de la computadora, en la cual se puede guardar un valor para usarlo posteriormente en un programa. Todas las variables deben declararse con un nombre y un tipo para poder utilizarlas. • El nombre de una variable permite al programa acceder al valor de la variable en memoria. Un nombre de variable puede ser cualquier identificador válido. • Al igual que otras instrucciones, las instrucciones de declaración de variables terminan con un punto y coma (;). • Un objeto Scanner (paquete java.util) permite a un programa leer datos para usarlos en éste. Los datos pueden provenir de muchas fuentes, como un archivo en disco o del usuario, a través del teclado. Antes de usar un objeto Scanner, el programa debe crearlo y especificar el origen de los datos. • Las variables deben inicializarse para poder usarlas en un programa. • La expresión new Scanner( System.in ) crea un objeto Scanner que lee datos desde el teclado. El objeto de entrada estándar, System.in, permite a las aplicaciones de Java leer los datos escritos por el usuario. • El tipo de datos int se utiliza para declarar variables que guardarán valores enteros. El rango de valores para un int es de –2,147,483,648 a +2,147,483,647. • Los tipos float y double especifican números reales, y el tipo char especifica datos de caracteres. Los números reales son números que contienen puntos decimales, como 3.4, 0.0 y –11.19. Las variables de tipo char representan caracteres individuales, como una letra mayúscula (por ejemplo, A), un dígito (por ejemplo, 7), un carácter especial (por ejemplo, * o %) o una secuencia de escape (por ejemplo, el carácter de nueva línea, \n). • Los tipos como int, float, double y char se conocen comúnmente como tipos primitivos o tipos integrados. Los nombres de los tipos primitivos son palabras clave; por ende, deben aparecer escritos sólo con letras minúsculas. • Un indicador pide al usuario que realice una acción específica. • El método nextInt de Scanner obtiene un entero para usarlo en un programa.

www.elsolucionario.net

Terminología

67

• El operador de asignación, =, permite al programa dar un valor a una variable. El operador = se llama operador binario, ya que tiene dos operandos. Una instrucción de asignación utiliza un operador de asignación para asignar un valor a una variable. • Las partes de las instrucciones que tienen valores se llaman expresiones. • El especificador de formato %d es un receptáculo para un valor int.

Sección 2.6 Conceptos acerca de la memoria • Los nombres de las variables corresponden a ubicaciones en la memoria de la computadora. Cada variable tiene un nombre, un tipo, un tamaño y un valor. • Cada vez que se coloca un valor en una ubicación de memoria, se sustituye al valor anterior en esa ubicación. El valor anterior se pierde.

Sección 2.7 Aritmética • La mayoría de los programas realizan cálculos aritméticos. Los operadores aritméticos son + (suma), – (resta), * (multiplicación), / (división) y % (residuo). • La división de enteros produce un cociente entero. • El operador residuo, %, produce el residuo después de la división. • Las expresiones aritméticas en Java deben escribirse en formato de línea recta. • Si una expresión contiene paréntesis anidados, el conjunto de paréntesis más interno se evalúa primero. • Java aplica los operadores en las expresiones aritméticas en una secuencia precisa, la cual se determina mediante las reglas de precedencia de los operadores. • Cuando decimos que los operadores se aplican de izquierda a derecha, nos referimos a su asociatividad. Algunos operadores se asocian de derecha a izquierda. • Los paréntesis redundantes en una expresión pueden hacer que ésta sea más clara.

Sección 2.8 Toma de decisiones: operadores de igualdad y relacionales • Una condición es una expresión que puede ser verdadera o falsa. La instrucción if de Java permite que un programa tome una decisión, con base en el valor de una condición. • Las condiciones en las instrucciones if se forman mediante el uso de los operadores de igualdad (== y !=) y relacionales (>, = y = “es mayor o igual a” residuo, operador (%) resta, operador (–) Scanner, clase secuencia de escape sensible a mayúsculas/minúsculas shell símbolo de MS-DOS Símbolo del sistema sintaxis System.in, objeto (entrada estándar) System.out, objeto (salida estándar) System.out.print, método System.out.printf, método System.out.println, método tamaño de una variable texto fijo en una cadena de formato tipo de una variable tipo integrado tipo primitivo tolerante a fallas true

ubicación de memoria ubicación de una variable valor de variable variable ventana de comandos ventana de Terminal void, palabra clave

Ejercicios de autoevaluación 2.1

Complete las siguientes oraciones: a) El cuerpo de cualquier método comienza con un(a) _____________ y termina con un(a) _____________. b) Toda instrucción termina con un _____________. c) La instrucción _____________ (presentada en este capítulo) se utiliza para tomar decisiones. d) _____________ indica el inicio de un comentario de fin de línea. e) ______________, ______________, ______________ y ______________ se conocen como espacio en blanco.

www.elsolucionario.net

Respuestas a los ejercicios de autoevaluación

69

f ) Las _____________ están reservadas para su uso en Java. g) Las aplicaciones en Java comienzan su ejecución en el método _____________. h) Los métodos _____________, _____________ y _____________ muestran información en la ventana de comandos. 2.2

Indique si cada una de las siguientes instrucciones es verdadera o falsa. Si es falsa, explique por qué. a) Los comentarios hacen que la computadora imprima el texto que va después de los caracteres // en la pantalla, al ejecutarse el programa. b) Todas las variables deben recibir un tipo cuando se declaran. c) Java considera que las variables numero y NuMeRo son idénticas. d) El operador residuo (%) puede utilizarse solamente con operandos enteros. e) Los operadores aritméticos *, /, %, + y – tienen todos el mismo nivel de precedencia.

2.3

Escriba instrucciones para realizar cada una de las siguientes tareas: a) Declarar las variables c, estaEsUnaVariable, q76354 y numero como de tipo int. b) Pedir al usuario que introduzca un entero. c) Recibir un entero como entrada y asignar el resultado a la variable int valor. Suponga que se puede utilizar la variable entrada tipo Scanner para recibir un valor del teclado. d) Si la variable numero no es igual a 7, mostrar "La variable numero no es igual a 7". e) Imprimir "Este es un programa en Java" en una línea de la ventana de comandos. f ) Imprimir "Este es un programa en Java" en dos líneas de la ventana de comandos. La primera línea debe terminar con es un. Use el método System.out.println. g) Imprimir "Este es un programa en Java" en dos líneas de la ventana de comandos. La primera línea debe terminar con es un. Use el método System.out.printf y dos especificadores de formato %s.

2.4

Identifique y corrija los errores en cada una de las siguientes instrucciones: a) if ( c < 7 ); System.out.println( "c es menor que 7" );

b)

if ( c => 7 ) System.out.println( "c es igual o mayor que 7" );

2.5

Escriba declaraciones, instrucciones o comentarios para realizar cada una de las siguientes tareas: a) Indicar que un programa calculará el producto de tres enteros. b) Crear un objeto Scanner que lea valores de la entrada estándar. c) Declarar las variables x, y, z y resultado de tipo int. d) Pedir al usuario que escriba el primer entero. e) Leer el primer entero del usuario y almacenarlo en la variable x. f ) Pedir al usuario que escriba el segundo entero. g) Leer el segundo entero del usuario y almacenarlo en la variable y. h) Pedir al usuario que escriba el tercer entero. i) Leer el tercer entero del usuario y almacenarlo en la variable z. j) Calcular el producto de los tres enteros contenidos en las variables x, y y z, y asignar el resultado a la variable resultado. k) Mostrar el mensaje "El producto es", seguido del valor de la variable resultado.

2.6 Utilizando las instrucciones que escribió en el ejercicio 2.5, escriba un programa completo que calcule e imprima el producto de tres enteros.

Respuestas a los ejercicios de autoevaluación 2.1 a) llave izquierda ({), llave derecha (}). b) punto y coma (;). c) if. d) //. e) Líneas en blanco, caracteres de espacio, caracteres de nueva línea y tabuladores. f ) palabras clave. g) main. h) System.out.print, System.out. println y System.out.printf. 2.2

a) Falso. Los comentarios no producen ninguna acción cuando el programa se ejecuta. Se utilizan para documentar programas y mejorar su legibilidad. b) Verdadero. c) Falso. Java es sensible a mayúsculas y minúsculas, por lo que estas variables son distintas.

www.elsolucionario.net

70

Capítulo 2

Introducción a las aplicaciones en Java

d) Falso. El operador residuo puede utilizarse también con operandos no enteros en Java. e) Falso. Los operadores *, / y % se encuentran en el mismo nivel de precedencia, y los operadores encuentran en un nivel menor de precedencia. 2.3

a)

+

y



se

int c, estaEsUnaVariable, q76354, numero;

o int c; int estaEsUnaVariable; int q76354; int numero;

b) c) d)

System.out.print( "Escriba un entero " ); valor = entrada.nextInt(); if ( numero != 7 ) System.out.println( "La variable numero no es igual a 7" );

e) f) g)

System.out.println( "Este es un programa en Java" ); System.out.println( "Este es un\n programa en Java" ); System.out.printf( "%s\%s\n", "Este es un", "programa en Java" );

2.4

Las soluciones al ejercicio de autoevaluación 2.4 son las siguientes: a) Error: hay un punto y coma después del paréntesis derecho de la condición (c < 7 ) en la instrucción if. Corrección: quite el punto y coma que va después del paréntesis derecho. [Nota: como resultado, la instrucción de salida se ejecutará, sin importar que la condición en la instrucción if sea verdadera]. b) Error: el operador relacional => es incorrecto. Corrección: cambie => a >=.

2.5

a) b) c)

// Calcula el producto de tres enteros Scanner entrada = new Scanner (System.in); int x, y, z, resultado;

o int x; int y; int z; int resultado;

d) e) f) g) h) i) j) k) l) 2.6

1 2 3 4 5 6 7 8 9 10 11 12 13

System.out.print( "Escriba el primer entero: " ); x = entrada.nextInt(); System.out.print( "Escriba el segundo entero: " ); y = entrada.nextInt(); System.out.print( "Escriba el tercer entero: " ); z = entrada.nextInt(); resultado = x * y * z; System.out.printf( "El producto es %d\n", resultado ); System.exit( 0 );

La solución para el ejercicio 2.6 es la siguiente: // Ejemplo 2.6: Producto.java // Calcular el producto de tres enteros. import java.util.Scanner; // el programa usa Scanner public class Producto { public static void main( String args[] ) { // crea objeto Scanner para obtener la entrada de la ventana de comandos Scanner entrada = new Scanner( System.in ); int x; // primer número introducido por el usuario int y; // segundo número introducido por el usuario

www.elsolucionario.net

Ejercicios

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

71

int z; // tercer número introducido por el usuario int resultado; // producto de los números System.out.print( "Escriba el primer entero: " ); // indicador de entrada x = entrada.nextInt(); // lee el primer entero System.out.print( "Escriba el segundo entero: " ); // indicador de entrada y = entrada.nextInt(); // lee el segundo entero System.out.print( "Escriba el tercer entero: " ); // indicador de entrada z = entrada.nextInt(); // lee el tercer entero resultado = x * y * z; // calcula el producto de los números System.out.printf( "El producto es %d\n", resultado ); } // fin del método main } // fin de la clase Producto

Escriba el primer entero: 10 Escriba el segundo entero: 20 Escriba el tercer entero: 30 El producto es 6000

Ejercicios 2.7

Complete las siguientes oraciones: a) _____________ se utilizan para documentar un programa y mejorar su legibilidad. b) Una decisión puede tomarse en un programa en Java con un(a) _____________. c) Los cálculos se realizan normalmente mediante instrucciones _____________. d) Los operadores aritméticos con la misma precedencia que la multiplicación son _____________ y _____ ________ . e) Cuando los paréntesis en una expresión aritmética están anidados, el conjunto _____________ de paréntesis se evalúa primero. f ) Una ubicación en la memoria de la computadora que puede contener distintos valores en diversos instantes de tiempo, durante la ejecución de un programa, se llama _____________.

2.8

Escriba instrucciones en Java que realicen cada una de las siguientes tareas: a) Mostrar el mensaje "Escriba un entero:", dejando el cursor en la misma línea. b) Asignar el producto de las variables b y c a la variable a. c) Indicar que un programa va a realizar un cálculo de nómina de muestra (es decir, usar texto que ayude a documentar un programa).

2.9

Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué. a) Los operadores en Java se evalúan de izquierda a derecha. b) Los siguientes nombres de variables son todos válidos: _barra_inferior_, m928134, t5, j7, sus_ventas$, su_$cuenta_total, a, b$, c, z y z2. c) Una expresión aritmética válida en Java sin paréntesis se evalúa de izquierda a derecha. d) Los siguientes nombres de variables son todos inválidos: 3g, 87, 67h2, h22 y 2h.

2.10

Suponiendo que x = 2 y y = 3, ¿qué muestra cada una de las siguientes instrucciones? a) System.out.printf( "x = %d\n", x ); b) System.out.printf( "El valor de %d + %d es %d\n", x, x, ( x + x ) ); c) System.out.printf( "x =" ); d) System.out.printf( "%d = %d\n", ( x + y ), ( y + x ) );

www.elsolucionario.net

72

Capítulo 2

Introducción a las aplicaciones en Java

2.11

¿Cuáles de las siguientes instrucciones de Java contienen variables, cuyos valores se modifican? a) p = i + j + k + 7; b) System.out.println( "variables cuyos valores se destruyen" ); c) System.out.println( "a = 5" ); d) valor = entrada.nextInt();

2.12

Dado que y = ax3+ 7, ¿cuáles de las siguientes instrucciones en Java son correctas para esta ecuación? a) y = a * x * x * x + 7; b) y = a * x * x * ( x + 7 ); c) y = ( a * x ) * x * ( x + 7 ); d) y = ( a * x ) * x * x + 7; e) y = a * ( x * x * x ) + 7; f ) y = a * x * ( x * x + 7 );

2.13 Indique el orden de evaluación de los operadores en cada una de las siguientes instrucciones en Java, y muestre el valor x después de ejecutar cada una de ellas: a) x = 7 + 3 * 6 / 2 – 1; b) x = 2 % 2 + 2 * 2 – 2 / 2; c) x = ( 3 * 9 * ( 3 + ( 9 * 3 / ( 3 ) ) ) ); 2.14 Escriba una aplicación que muestre los números del 1 al 4 en la misma línea, con cada par de números adyacentes separado por un espacio. Escriba el programa utilizando las siguientes técnicas: a) Utilizando una instrucción System.out.println. b) Utilizando cuatro instrucciones System.out.print. c) Utilizando una instrucción System.out.printf. 2.15 Escriba una aplicación que pida al usuario que escriba dos números, que obtenga los números del usuario e imprima la suma, producto, diferencia y cociente (división) de los números. Use las técnicas que se muestran en la figura 2.7. 2.16 Escriba una aplicación que pida al usuario que escriba dos enteros, que obtenga los números del usuario y muestre el número más grande, seguido de las palabras "es más grande". Si los números son iguales, imprima el mensaje "Estos números son iguales". Utilice las técnicas que se muestran en la figura 2.15. 2.17 Escriba una aplicación que reciba tres enteros del usuario y muestre la suma, promedio, producto, menor y mayor de esos números. Utilice las técnicas que se muestran en la figura 2.15. [Nota: el cálculo del promedio en este ejercicio debe resultar en una representación entera del promedio. Por lo tanto, si la suma de los valores es 7, el promedio debe ser 2, no 2.3333...]. 2.18 Escriba una aplicación que muestre un cuadro, un óvalo, una flecha y un diamante usando asteriscos (*), como se muestra a continuación: ********* * * * * * * * * * * * * * * *********

2.19

*** *

*

* * * * *

* * * * * *

* ***

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

* * * * * * * *

* *

* * * * * *

¿Qué imprime el siguiente código? System.out.println( "*\n**\n***\n****\n*****" );

2.20

¿Qué imprime el siguiente código? System.out.println( "*" ); System.out.println( "***" ); System.out.println( "*****" ); System.out.println( "****" ); System.out.println( "**" );

www.elsolucionario.net

Ejercicios

2.21

73

¿Qué imprime el siguiente código? System.out.print( "*" ); System.out.print( "***" ); System.out.print( "*****" ); System.out.print( "****" ); System.out.println( "**" );

2.22

¿Qué imprime el siguiente código? System.out.print( "*" ); System.out.println( "***" ); System.out.println( "*****" ); System.out.print( "****" ); System.out.println( "**" );

2.23

¿Qué imprime el siguiente código? System.out.printf( "%s\n%s\n%s\n", "*", "***", "*****" );

2.24 Escriba una aplicación que lea cinco enteros y que determine e imprima los enteros mayor y menor en el grupo. Use solamente las técnicas de programación que aprendió en este capítulo. 2.25 Escriba una aplicación que lea un entero y que determine e imprima si es impar o par. [Sugerencia: use el operador residuo. Un número par es un múltiplo de 2. Cualquier múltiplo de 2 deja un residuo de 0 cuando se divide entre 2]. 2.26 Escriba una aplicación que lea dos enteros, determine si el primero es un múltiplo del segundo e imprima el resultado. [Sugerencia: use el operador residuo]. 2.27

Escriba una aplicación que muestre un patrón de tablero de damas, como se muestra a continuación:

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

2.28 He aquí un adelanto. En este capítulo, aprendió sobre los enteros y el tipo int. Java también puede representar números de punto flotante que contienen puntos decimales, como 3.14159. Escriba una aplicación que reciba del usuario el radio de un círculo como un entero, y que imprima el diámetro, la circunferencia y el área del círculo mediante el uso del valor de punto flotante 3.14159 para π. Use las técnicas que se muestran en la figura 2.7. [Nota: también puede utilizar la constante predefinida Math.PI para el valor de π. Esta constante es más precisa que el valor 3.14159. La clase Math se define en el paquete java.lang. Las clases en este paquete se importan de manera automática, por lo que no necesita importar la clase Math mediante la instrucción import para usarla]. Use las siguientes fórmulas (r es el radio): diámetro = 2r circunferencia = 2πr área = πr 2

No almacene los resultados de cada cálculo en una variable. En vez de ello, especifique cada cálculo como el valor que se imprimirá en una instrucción System.out.printf. Observe que los valores producidos por los cálculos del área y la circunferencia son números de punto flotante. Dichos valores pueden imprimirse con el especificador de formato %f en una instrucción System.out.printf. En el capítulo 3 aprenderá más acerca de los números de punto flotante. 2.29 He aquí otro adelanto. En este capítulo, aprendió acerca de los enteros y el tipo int. Java puede también representar letras en mayúsculas, en minúsculas y una considerable variedad de símbolos especiales. Cada carácter tiene su correspondiente representación entera. El conjunto de caracteres que utiliza una computadora, y las correspondientes representaciones enteras de esos caracteres, se conocen como el conjunto de caracteres de esa computadora. Usted puede indicar un valor de carácter en un programa con sólo encerrar ese carácter entre comillas sencillas, como en 'A'.

www.elsolucionario.net

74

Capítulo 2

Introducción a las aplicaciones en Java

Usted puede determinar el equivalente entero de un carácter si antepone a ese carácter la palabra (int), como en (int) 'A'

Esta forma se conoce como operador de conversión de tipo. (Hablaremos más sobre estos operadores en el capítulo 4). La siguiente instrucción imprime un carácter y su equivalente entero: System.out.printf( "El caracter %c tiene el valor %d\n", 'A', ( (int) 'A' ) );

Cuando se ejecuta esta instrucción, muestra el carácter A y el valor 65 (del conjunto de caracteres conocido como Unicode®) como parte de la cadena. Observe que el especificador de formato %c es un receptáculo para un carácter (en este caso, el carácter 'A').

Utilizando instrucciones similares a la mostrada anteriormente en este ejercicio, escriba una aplicación que muestre los equivalentes enteros de algunas letras en mayúsculas, en minúsculas, dígitos y símbolos especiales. Muestre los equivalentes enteros de los siguientes caracteres: A B C a b c 0 1 2 $ * + / y el carácter en blanco. 2.30 Escriba una aplicación que reciba del usuario un número compuesto por cinco dígitos, que separe ese número en sus dígitos individuales y los imprima, cada uno separado de los demás por tres espacios. Por ejemplo, si el usuario escribe el número 42339, el programa debe imprimir 4

2

3

3

9

Suponga que el usuario escribe el número correcto de dígitos. ¿Qué ocurre cuando ejecuta el programa y escribe un número con más de cinco dígitos? ¿Qué ocurre cuando ejecuta el programa y escribe un número con menos de cinco dígitos? [Sugerencia: es posible hacer este ejercicio con las técnicas que aprendió en este capítulo. Necesitará utilizar los operadores de división y residuo para “seleccionar” cada dígito]. 2.31 Utilizando sólo las técnicas de programación que aprendió en este capítulo, escriba una aplicación que calcule los cuadrados y cubos de los números del 0 al 10, y que imprima los valores resultantes en formato de tabla, como se muestra a continuación. [Nota: Este programa no requiere de ningún tipo de entrada por parte del usuario]. numero 0 1 2 3 4 5 6 7 8 9 10

cuadrado 0 1 4 9 16 25 36 49 64 81 100

cubo 0 1 8 27 64 125 216 343 512 729 1000

2.32 Escriba un programa que reciba cinco números, y que determine e imprima la cantidad de números negativos, positivos, y la cantidad de ceros recibidos.

www.elsolucionario.net

3 Usted verá algo nuevo. Dos cosas. Y las llamo Cosa Uno y Cosa Dos.

Introducción a las clases y los objetos

—Dr. Theodor Seuss Geisel.

Nada puede tener valor sin ser un objeto de utilidad. —Karl Marx

OBJETIVOS

Sus sirvientes públicos le sirven bien.

En este capítulo aprenderá a: Q

Comprender qué son las clases, los objetos, los métodos y las variables de instancia.

Q

Declarar una clase y utilizarla para crear un objeto.

Q

Declarar métodos en una clase para implementar los comportamientos de ésta.

Q

Declarar variables de instancia en una clase para implementar los atributos de ésta.

Q

Saber cómo llamar a los métodos de un objeto para hacer que realicen sus tareas.

Q

Conocer las diferencias entre las variables de instancia de una clase y las variables locales de un método.

Q

Utilizar un constructor para asegurar que los datos de un objeto se inicialicen cuando se cree el objeto.

Q

Conocer las diferencias entre los tipos primitivos y los tipos por referencia.

—Adlai E. Stevenson

Saber cómo responder a alguien que habla, contestar a alguien que envía un mensaje. —Amenemope

www.elsolucionario.net

Pla n g e ne r a l

76

Capítulo 3

Introducción a las clases y los objetos

3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10

Introducción Clases, objetos, métodos y variables de instancia Declaración de una clase con un método e instanciamiento de un objeto de una clase Declaración de un método con un parámetro Variables de instancia, métodos establecer y métodos obtener Comparación entre tipos primitivos y tipos por referencia Inicialización de objetos mediante constructores Números de punto flotante y el tipo double (Opcional) Ejemplo práctico de GUI y gráficos: uso de cuadros de diálogo (Opcional) Ejemplo práctico de Ingeniería de Software: identificación de las clases en un documento de requerimientos 3.11 Conclusión

Resumen | Terminología | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

3.1 Introducción En la sección 1.16 le presentamos la terminología básica y los conceptos acerca de la programación orientada a objetos. El capítulo 2 comenzó a utilizar esos conceptos para crear aplicaciones simples que mostraran mensajes al usuario, que obtuvieran información del usuario, realizaran cálculos y tomaran decisiones. Una característica común de todas las aplicaciones del capítulo 2 fue que todas las instrucciones que realizaban tareas se encontraban en el método main. Por lo general, las aplicaciones que usted desarrollará en este libro consistirán de dos o más clases, cada una de las cuales contendrá dos o más métodos. Si usted se integra a un equipo de desarrollo en la industria, podría trabajar en aplicaciones que contengan cientos, o incluso hasta miles de clases. En este capítulo presentaremos un marco de trabajo simple para organizar las aplicaciones orientadas a objetos en Java. Primero explicaremos el concepto de las clases mediante el uso de un ejemplo real. Después presentaremos cinco aplicaciones completas para demostrarle cómo crear y utilizar sus propias clases. Los primeros cuatro ejemplos empiezan nuestro ejemplo práctico acerca de cómo desarrollar una clase tipo libro de calificaciones, que los instructores pueden utilizar para mantener las calificaciones de las pruebas de sus estudiantes. Durante los siguientes capítulos ampliaremos este ejemplo práctico y culminaremos con la versión que se presenta en el capítulo 7, Arreglos. El último ejemplo en este capítulo introduce los números de punto flotante (es decir, números que contienen puntos decimales, como 0.0345, –7.23 y 100.7) en el contexto de una clase tipo cuenta bancaria, la cual mantiene el saldo de un cliente.

3.2 Clases, objetos, métodos y variables de instancia Comenzaremos con una analogía simple, para ayudarle a comprender el concepto de las clases y su contenido. Suponga que desea conducir un automóvil y, para hacer que aumente su velocidad, debe presionar el pedal del acelerador. ¿Qué debe ocurrir antes de que pueda hacer esto? Bueno, antes de poder conducir un automóvil, alguien tiene que diseñarlo. Por lo general, un automóvil empieza en forma de dibujos de ingeniería, similares a los planos de construcción que se utilizan para diseñar una casa. Estos dibujos de ingeniería incluyen el diseño del pedal del acelerador, para que el automóvil aumente su velocidad. El pedal “oculta” los complejos mecanismos que se encargan de que el automóvil aumente su velocidad, de igual forma que el pedal del freno “oculta” los mecanismos que disminuyen la velocidad del automóvil y por otro lado, el volante “oculta” los mecanismos que hacen que el automóvil de vuelta. Esto permite que las personas con poco o nada de conocimiento acerca de cómo funcionan los motores puedan conducir un automóvil con facilidad. Desafortunadamente, no puede conducir los dibujos de ingeniería de un auto. Antes de poder conducir un automóvil, éste debe construirse a partir de los dibujos de ingeniería que lo describen. Un automóvil completo tendrá un pedal acelerador verdadero para hacer que aumente su velocidad, pero aún así no es suficiente; el automóvil no acelerará por su propia cuenta, así que el conductor debe oprimir el pedal del acelerador. Ahora utilizaremos nuestro ejemplo del automóvil para introducir los conceptos clave de programación de esta sección. Para realizar una tarea en una aplicación se requiere un método. El método describe los mecanismos

www.elsolucionario.net

3.3

Declaración de una clase con un método e instanciamiento de un objeto de una clase

77

que se encargan de realizar sus tareas; y oculta al usuario las tareas complejas que realiza, de la misma forma que el pedal del acelerador de un automóvil oculta al conductor los complejos mecanismos para hacer que el automóvil vaya más rápido. En Java, empezamos por crear una unidad de aplicación llamada clase para alojar a un método, así como los dibujos de ingeniería de un automóvil alojan el diseño del pedal del acelerador. En una clase se proporcionan uno o más métodos, los cuales están diseñados para realizar las tareas de esa clase. Por ejemplo, una clase que representa a una cuenta bancaria podría contener un método para depositar dinero en una cuenta, otro para retirar dinero de una cuenta y un tercero para solicitar el saldo actual de la cuenta. Así como no podemos conducir un dibujo de ingeniería de un automóvil, tampoco podemos “conducir” una clase. De la misma forma que alguien tiene que construir un automóvil a partir de sus dibujos de ingeniería para poder conducirlo, también debemos construir un objeto de una clase para poder hacer que un programa realice las tareas que la clase le describe cómo realizar. Ésta es una de las razones por las cuales Java se conoce como un lenguaje de programación orientado a objetos. Cuando usted conduce un automóvil, si oprime el pedal del acelerador se envía un mensaje al automóvil para que realice una tarea-hacer que el automóvil vaya más rápido. De manera similar, se envían mensajes a un objeto; cada mensaje se conoce como la llamada a un método, e indica a un método del objeto que realice su tarea. Hasta ahora, hemos utilizado la analogía del automóvil para introducir las clases, los objetos y los métodos. Además de las capacidades con las que cuenta un automóvil, también tiene muchos atributos como su color, el número de puertas, la cantidad de gasolina en su tanque, su velocidad actual y el total de kilómetros recorridos (es decir, la lectura de su odómetro). Al igual que las capacidades del automóvil, estos atributos se representan como parte del diseño en sus diagramas de ingeniería. Cuando usted conduce un automóvil, estos atributos siempre están asociados con él. Cada uno mantiene sus propios atributos. Por ejemplo, cada conductor sabe cuánta gasolina tiene en su propio tanque, pero no cuánta hay en los tanques de otros automóviles. De manera similar, un objeto tiene atributos que lleva consigo cuando se utiliza en un programa. Éstos se especifican como parte de la clase del objeto. Por ejemplo, un objeto tipo cuenta bancaria tiene un atributo llamado saldo, el cual representa la cantidad de dinero en la cuenta. Cada objeto tipo cuenta bancaria conoce el saldo en la cuenta que representa, pero no los saldos de las otras cuentas en el banco. Los atributos se especifican mediante las variables de instancia de la clase. El resto de este capítulo presenta ejemplos que demuestran los conceptos que presentamos aquí, dentro del contexto de la analogía del automóvil. Los primeros cuatro ejemplos se encargan de construir en forma incremental una clase llamada LibroCalificaciones para demostrar estos conceptos: 1. El primer ejemplo presenta una clase llamada LibroCalificaciones, con un método que simplemente muestra un mensaje de bienvenida cuando se le llama. Después le mostraremos cómo crear un objeto de esa clase y cómo llamarlo, para que muestre el mensaje de bienvenida. 2. El segundo ejemplo modifica el primero, al permitir que el método reciba el nombre de un curso como argumento, y al mostrar ese nombre como parte del mensaje de bienvenida. 3. El tercer ejemplo muestra cómo almacenar el nombre del curso en un objeto tipo LibroCalificaciones. Para esta versión de la clase, también le mostraremos cómo utilizar los métodos para establecer el nombre del curso y obtener este nombre. 4. El cuarto ejemplo demuestra cómo pueden inicializarse los datos en un objeto tipo LibroCalificaciones, a la hora de crear el objeto; el constructor de la clase se encarga de realizar el proceso de inicialización. El último ejemplo en el capítulo presenta una clase llamada Cuenta, la cual refuerza los conceptos presentados en los primeros cuatro ejemplos, e introduce los números de punto flotante. Para este fin, presentamos una clase llamada Cuenta, la cual representa una cuenta bancaria y mantiene su saldo como un número de punto flotante. La clase contiene dos métodos —uno para acreditar un depósito a la cuenta, con lo cual se incrementa el saldo, y otro para obtener el saldo. El constructor de la clase permite inicializar el saldo de cada objeto tipo Cuenta, a la hora de crear el objeto. Crearemos dos objetos tipo Cuenta y haremos depósitos en cada uno de ellos, para mostrar que cada objeto mantiene su propio saldo. El ejemplo también demuestra cómo recibir e imprimir en pantalla números de punto flotante.

3.3 Declaración de una clase con un método e instanciamiento de un objeto de una clase Comenzaremos con un ejemplo que consiste en las clases LibroCalificaciones (figura 3.1) y PruebaLibroCalificaciones (figura 3.2). La clase LibroCalificaciones (declarada en el archivo LibroCalificaciones.java)

www.elsolucionario.net

78

Capítulo 3

Introducción a las clases y los objetos

se utilizará para mostrar un mensaje en la pantalla (figura 3.2), para dar la bienvenida al instructor a la aplicación del libro de calificaciones. La clase PruebaLibroCalificaciones (declarada en el archivo PruebaLibroCalificaciones.java) es una clase de aplicación en la que el método main utilizará a la clase LibroCalificaciones. Cada declaración de clase que comienza con la palabra clave public debe almacenarse en un archivo que tenga el mismo nombre que la clase, y que termine con la extensión de archivo .java. Por lo tanto, las clases LibroCalificaciones y PruebaLibroCalificaciones deben declararse en archivos separados, ya que cada clase se declara como public.

Error común de programación 3.1 Declarar más de una clase public en el mismo archivo es un error de compilación.

La clase LibroCalificaciones La declaración de la clase LibroCalificaciones (figura 3.1) contiene un método llamado mostrarMensaje (líneas 7-10), el cual muestra un mensaje en la pantalla. La línea 9 de la clase realiza el trabajo de mostrar el mensaje. Recuerde que una clase es como un plano de construcción; necesitamos crear un objeto de esta clase y llamar a su método para hacer que se ejecute la línea 9 y que muestre su mensaje. La declaración de la clase empieza en la línea 4. La palabra clave public es un modificador de acceso. Por ahora, simplemente declararemos cada clase como public. Toda declaración de clase contiene la palabra clave class, seguida inmediatamente por el nombre de la clase. El cuerpo de toda clase se encierra entre una llave izquierda y una derecha ({ y }), como en las líneas 5 y 12 de la clase LibroCalificaciones. En el capítulo 2, cada clase que declaramos tenía un método llamado main. La clase LibroCalificaciones también tiene un método: mostrarMensaje (líneas 7-10). Recuerde que main es un método especial, que siempre es llamado, automáticamente, por la Máquina Virtual de Java (JVM) a la hora de ejecutar una aplicación. La mayoría de los métodos no se llaman en forma automática. Como veremos en breve, es necesario llamar al método mostrarMensaje para decirle que haga su trabajo. La declaración del método comienza con la palabra clave public para indicar que el método está “disponible al público”; es decir, los métodos de otras clases pueden llamarlo desde el exterior del cuerpo de la declaración de la clase. La palabra clave void indica que este método realizará una tarea pero no devolverá (es decir, regresará) información al método que hizo la llamada cuando complete su tarea. Ya hemos utilizado métodos que devuelven información; por ejemplo, en el capítulo 2 utilizó el método nextInt de Scanner para recibir un entero escrito por el usuario desde el teclado. Cuando nextInt recibe un valor de entrada, devuelve ese valor para utilizarlo en el programa. El nombre del método, mostrarMensaje, va después del tipo de valor de retorno. Por convención, los nombres de los métodos comienzan con una letra minúscula, y el resto de las palabras en el nombre empiezan con letra mayúscula. Los paréntesis después del nombre del método indican que éste es un método. Un conjunto vacío de paréntesis, como se muestra en la línea 7, indica que este método no requiere información adicional para realizar su tarea. La línea 7 se conoce comúnmente como el encabezado del método. El cuerpo de cada método se delimita mediante una llave izquierda y una llave derecha ({ y }), como en las líneas 8 y 10.

1 2 3 4 5 6 7 8 9 10 11 12

// Fig. 3.1: LibroCalificaciones.java // Declaración de una clase con un método. public class LibroCalificaciones { // muestra un mensaje de bienvenida al usuario de LibroCalificaciones public void mostrarMensaje() { System.out.println( “Bienvenido al Libro de calificaciones!” );

} // fin del método mostrarMensaje } // fin de la clase LibroCalificaciones

Figura 3.1 | Declaración de una clase con un método.

www.elsolucionario.net

3.3

Declaración de una clase con un método e instanciamiento de un objeto de una clase

79

El cuerpo de un método contiene una o varias instrucciones que realizan su trabajo. En este caso, el método contiene una instrucción (línea 9) que muestra el mensaje "Bienvenido al Libro de calificaciones!", seguido de una nueva línea en la ventana de comandos. Una vez que se ejecuta esta instrucción, el método ha completado su trabajo. A continuación, nos gustaría utilizar la clase LibroCalificaciones en una aplicación. Como aprendió en el capítulo 2, el método main empieza la ejecución de todas las aplicaciones. Una clase que contiene el método main es una aplicación de Java. Dicha clase es especial, ya que la JVM puede utilizar a main como un punto de entrada para empezar la ejecución. La clase LibroCalificaciones no es una aplicación, ya que no contiene a main. Por lo tanto, si trata de ejecutar LibroCalificaciones escribiendo java LibroCalificaciones en la ventana de comandos, recibirá un mensaje de error como este: Exception in thread "main" java.lang.NoSuchMethodError: main

Esto no fue un problema en el capítulo 2, ya que cada clase que declaramos tenía un método main. Para corregir este problema con la clase LibroCalificaciones, debemos declarar una clase separada que contenga un método main, o colocar un método main en la clase LibroCalificaciones. Para ayudarlo a prepararse para los programas más extensos que encontrará más adelante en este libro y en la industria, utilizamos una clase separada (PruebaLibroCalificaciones en este ejemplo) que contiene el método main para probar cada nueva clase que vayamos a crear en este capítulo.

La clase PruebaLibroCalificaciones La declaración de la clase PruebaLibroCalificaciones (figura 3.2) contiene el método main que controlará la ejecución de nuestra aplicación. Cualquier clase que contiene el método main, declarado como se muestra en la línea 7, puede utilizarse para ejecutar una aplicación. La declaración de la clase PruebaLibroCalificaciones empieza en la línea 4 y termina en la línea 16. La clase sólo contiene un método main, algo común en muchas clases que empiezan la ejecución de una aplicación. Las líneas 7 a la 14 declaran el método main. En el capítulo 2 vimos que el encabezado main debe aparecer como se muestra en la línea 7; en caso contrario, no se ejecutará la aplicación. Una parte clave para permitir que la JVM localice y llame al método main para empezar la ejecución de la aplicación es la palabra clave static (línea 7), la cual indica que main es un método static. Un método static es especial, ya que puede llamarse sin tener que crear primero un objeto de la clase en la cual se declara ese método. En el capítulo 6, Métodos: un análisis más detallado, explicaremos a detalle los métodos static.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

// Fig. 3.2: PruebaLibroCalificaciones.java // Crea un objeto LibroCalificaciones y llama a su método mostrarMensaje. public class PruebaLibroCalificaciones { // el método main empieza la ejecución del programa public static void main( String args[] ) { // crea un objeto LibroCalificaciones y lo asigna a miLibroCalificaciones LibroCalificaciones miLibroCalificaciones = new LibroCalificaciones(); // llama al método mostrarMensaje de miLibroCalificaciones miLibroCalificaciones.mostrarMensaje(); } // fin de main } // fin de la clase PruebaLibroCalificaciones

Bienvenido al Libro Calificaciones!

Figura 3.2 | Cómo crear un objeto de la clase LibroCalificaciones y llamar a su método mostrarMensaje.

www.elsolucionario.net

80

Capítulo 3

Introducción a las clases y los objetos

En esta aplicación nos gustaría llamar al método mostrarMensaje de la clase LibroCalificaciones para mostrar el mensaje de bienvenida en la ventana de comandos. Por lo general, no podemos llamar a un método que pertenece a otra clase, sino hasta crear un objeto de esa clase, como se muestra en la línea 10. Empezaremos por declarar la variable miLibroCalificaciones. Observe que el tipo de la variable es LibroCalificaciones; la clase que declaramos en la figura 3.1. Cada nueva clase que creamos se convierte en un nuevo tipo, que puede usarse para declarar variables y crear objetos. Los programadores pueden declarar nuevos tipos de clases según lo necesiten; ésta es una razón por la cual Java se conoce como un lenguaje extensible. La variable miLibroCalificaciones se inicializa con el resultado de la expresión de creación de instancia de clase new LibroCalificaciones(). La palabra clave new crea un nuevo objeto de la clase especificada a la derecha de la palabra clave (es decir, LibroCalificaciones). Los paréntesis a la derecha de LibroCalificaciones son obligatorios. Como veremos en la sección 3.7, esos paréntesis en combinación con el nombre de una clase representan una llamada a un constructor, que es similar a un método, pero se utiliza sólo cuando se crea un objeto, para inicializar los datos de éste. En esa sección verá que los datos pueden colocarse entre paréntesis para especificar los valores iniciales para los datos del objeto. Por ahora, sólo dejaremos los paréntesis vacíos. Así como podemos usar el objeto System.out para llamar a los métodos print, printf y println, también podemos usar el objeto miLibroCalificaciones para llamar al método mostrarMensaje. La línea 13 llama al método mostrarMensaje (líneas 7-10 de la figura 3.1), usando miLibroCalificaciones seguida de un separador punto (.), el nombre del método mostrarMensaje y un conjunto vacío de paréntesis. Esta llamada hace que el método mostrarMensaje realice su tarea. La llamada a este método difiere de las del capítulo 2 en las que se mostraba la información en una ventana de comandos; cada una de estas llamadas al método proporcionaban argumentos que especificaban los datos a mostrar. Al inicio de la línea 13, “miLibroCalificaciones”. Indica que main debe utilizar el objeto miLibroCalificaciones que se creó en la línea 10. La línea 7 de la figura 3.1 indica que el método mostrarMensaje tiene una lista de parámetros vacía; es decir, mostrarMensaje no requiere información adicional para realizar su tarea. Por esta razón, la llamada al método (línea 13 de la figura 3.2) especifica un conjunto vacío de paréntesis después del nombre del método, para indicar que no se van a pasar argumentos al método mostrarMensaje. Cuando el método mostrarMensaje completa su tarea, el método main continúa su ejecución en la línea 14. Éste es el final del método main, por lo que el programa termina.

Compilación de una aplicación con varias clases Debe compilar las clases de las figuras 3.1 y 3.2 antes de poder ejecutar la aplicación. Primero, cambie al directorio que contiene los archivos de código fuente de la aplicación. Después, escriba el comando javac LibroCalificaciones.java PruebaLibroCalificaciones.java

para compilar ambas clases a la vez. Si el directorio que contiene la aplicación sólo incluye los archivos de esta aplicación, puede compilar todas las clases que haya en el directorio con el comando javac *.java

El asterisco (*) en *.java indica que deben compilarse todos los archivos en el directorio actual que terminen con la extensión de nombre de archivo “.java”.

Diagrama de clases de UML para la clase LibroCalificaciones La figura 3.3 presenta un diagrama de clases de UML para la clase LibroCalificaciones de la figura 3.1. En la sección 1.16 vimos que UML es un lenguaje gráfico, utilizado por los programadores para representar sistemas orientados a objetos en forma estandarizada. En UML, cada clase se modela en un diagrama de clases en forma de un rectángulo con tres componentes. El compartimiento superior contiene el nombre de la clase, centrado en forma horizontal y en negrita. El compartimiento de en medio contiene los atributos de la clase, que en Java corresponden a las variables de instancia. En la figura 3.3, el compartimiento de en medio está vacío, ya que la versión de la clase LibroCalificaciones en la figura 3.1 no tiene atributos. El compartimiento inferior contiene las operaciones de la clase, que en Java corresponden a los métodos. Para modelar las operaciones, UML lista el nombre de la operación precedido por un modificador de acceso y seguido de un conjunto de paréntesis. La clase LibroCalificaciones tiene un solo método llamado mostrarMensaje, por lo que el compartimiento inferior de la figura 3.3 lista una operación con este nombre. El método mostrarMensaje no requiere información adicional para realizar sus tareas, por lo que los paréntesis que van después del nombre del método en el diagrama de

www.elsolucionario.net

3.4

Declaración de un método con un parámetro

81

LibroCalificaciones

+ mostrarMensaje( )

Figura 3.3 | Diagrama de clases de UML, el cual indica que la clase LibroCalificaciones tiene una operación public llamada mostrarMensaje. clases están vacíos, de igual forma que como aparecieron en la declaración del método, en la línea 7 de la figura 3.1. El signo más (+) que va antes del nombre de la operación indica que mostrarMensaje es una operación public en UML (es decir, un método public en Java). Utilizaremos los diagramas de clases de UML a menudo para sintetizar los atributos y las operaciones de una clase.

3.4 Declaración de un método con un parámetro En nuestra analogía del automóvil de la sección 3.2, hablamos sobre el hecho de que al oprimir el pedal del acelerador se envía un mensaje al automóvil para que realice una tarea: hacer que vaya más rápido. Pero ¿qué tan rápido debería acelerar el automóvil? Como sabe, entre más oprima el pedal, mayor será la aceleración del automóvil. Por lo tanto, el mensaje para el automóvil en realidad incluye tanto la tarea a realizar como información adicional que ayuda al automóvil a ejecutar su tarea. A la información adicional se le conoce como parámetro; el valor del parámetro ayuda al automóvil a determinar qué tan rápido debe acelerar. De manera similar, un método puede requerir uno o más parámetros que representan la información adicional que necesita para realizar su tarea. La llamada a un método proporciona valores (llamados argumentos) para cada uno de los parámetros de ese método. Por ejemplo, el método System.out.println requiere un argumento que especifica los datos a mostrar en una ventana de comandos. De manera similar, para realizar un depósito en una cuenta bancaria, un método llamado deposito especifica un parámetro que representa el monto a depositar. Cuando se hace una llamada al método deposito, se asigna al parámetro del método un valor como argumento, que representa el monto a depositar. Entonces, el método realiza un depósito por ese monto. Nuestro siguiente ejemplo declara la clase LibroCalificaciones (figura 3.4), con un método mostrarMensaje que muestra el nombre del curso como parte del mensaje de bienvenida (en la figura 3.5 podrá ver la ejecución de ejemplo). El nuevo método mostrarMensaje requiere un parámetro que representa el nombre del curso a imprimir en pantalla. Antes de hablar sobre las nuevas características de la clase LibroCalificaciones, veamos cómo se utiliza la nueva clase desde el método main de la clase PruebaLibroCalificaciones (figura 3.5). La línea 12 crea un objeto Scanner llamado entrada, para recibir el nombre del curso escrito por el usuario. La línea 15 crea un objeto de la clase LibroCalificaciones y lo asigna a la variable miLibroCalificaciones. La línea 18 pide al usuario que escriba el nombre de un curso. La línea 19 lee el nombre que introduce el usuario y lo asigna a la variable nombreDelCurso, mediante el uso del método nextLine de Scanner para realizar la operación de entrada. El

1 2 3 4 5 6 7 8 9 10 11 12 13

// Fig. 3.4: LibroCalificaciones.java // Declaración de una clase con un método que tiene un parámetro. public class LibroCalificaciones { // muestra un mensaje de bienvenida al usuario de LibroCalificaciones public void mostrarMensaje( String nombreDelCurso ) { System.out.printf( “Bienvenido al libro de calificaciones para\n%s!\n”, nombreDelCurso ); } // fin del método mostrarMensaje } // fin de la clase LibroCalificaciones

Figura 3.4 | Declaración de una clase con un método que tiene un parámetro.

www.elsolucionario.net

82

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

Capítulo 3

Introducción a las clases y los objetos

// Fig. 3.5: PruebaLibroCalificaciones.java // Crea un objeto LibroCalificaciones y pasa un objeto String // a su método mostrarMensaje. import java.util.Scanner; // el programa usa la clase Scanner public class PruebaLibroCalificaciones { // el método main empieza la ejecución del programa public static void main( String args[] ) { // crea un objeto Scanner para obtener la entrada de la ventana de comandos Scanner entrada = new Scanner( System.in ); // crea un objeto LibroCalificaciones y lo asigna a miLibroCalificaciones LibroCalificaciones miLibroCalificaciones = new LibroCalificaciones(); // pide y recibe el nombre del curso como entrada System.out.println( “Escriba el nombre del curso:” ); String nombreDelCurso = entrada.nextLine(); // lee una línea de texto System.out.println(); // imprime una línea en blanco // llama al método mostrarMensaje de miLibroCalificaciones // y pasa nombreDelCurso como argumento miLibroCalificaciones.mostrarMensaje( nombreDelCurso ); } // fin de main } // fin de la clase PruebaLibroCalificaciones

Escriba el nombre del curso: CS101 Introduccion a la programacion en Java Bienvenido al libro de calificaciones para CS101 Introduccion a la programacion en Java!

Figura 3.5 | Cómo crear un objeto LibroCalificaciones y pasar un objeto String a su método mostrarMensaje. usuario escribe el nombre del curso y oprime Intro para enviarlo al programa. Observe que al oprimir Intro se inserta un carácter de nueva línea al final de los caracteres escritos por el usuario. El método nextLine lee los caracteres que escribió el usuario hasta encontrar el carácter de nueva línea, y después devuelve un objeto String que contiene los caracteres hasta, pero sin incluir, la nueva línea. El carácter de nueva línea se descarta. La clase Scanner también cuenta con un método similar (next) para leer palabras individuales. Cuando el usuario oprime Intro después de escribir la entrada, el método next lee caracteres hasta encontrar un carácter de espacio en blanco (espacio, tabulador o nueva línea), y después devuelve un objeto String que contiene los caracteres hasta, pero sin incluir, el carácter de espacio en blanco (que se descarta). No se pierde toda la información que va después del primer carácter de espacio en blanco; estará disponible para que la lean otras instrucciones que llamen a los métodos de Scanner, más adelante en el programa. La línea 24 llama al método mostrarMensaje de miLibroCalificaciones. La variable nombreDelCurso entre paréntesis es el argumento que se pasa al método mostrarMensaje, para que éste pueda realizar su tarea. El valor de la variable nombreDelCurso en main se convierte en el valor del parámetro nombreDelCurso del método mostrarMensaje, en la línea 7 de la figura 3.4. Al ejecutar esta aplicación, observe que el método mostrarMensaje imprime en pantalla el nombre que usted escribió como parte del mensaje de bienvenida (figura 3.5).

Observación de ingeniería de software 3.1 Por lo general, los objetos se crean mediante el uso de new. Una excepción es la literal de cadena que está encerrada entre comillas, como “hola”. Las literales de cadena son referencias a objetos String que Java crea de manera implícita.

www.elsolucionario.net

3.4

Declaración de un método con un parámetro

83

Más sobre los argumentos y los parámetros Al declarar un método, debe especificar si el método requiere datos para realizar su tarea. Para ello es necesario colocar información adicional en la lista de parámetros del método, la cual se encuentra en los paréntesis que van después del nombre del método. La lista de parámetros puede contener cualquier número de parámetros, incluso ninguno. Los paréntesis vacíos después del nombre del método (como en la figura 3.1, línea 7) indican que un método no requiere parámetros. En la figura 3.4, la lista de parámetros de mostrarMensaje (línea 7) declara que el método requiere un parámetro. Cada parámetro debe especificar un tipo y un identificador. En este caso, el tipo String y el identificador nombreDelCurso indican que el método mostrarMensaje requiere un objeto String para realizar su tarea. En el instante en que se llama al método, el valor del argumento en la llamada se asigna al parámetro correspondiente (en este caso, nombreDelCurso) en el encabezado del método. Después, el cuerpo del método utiliza el parámetro nombreDelCurso para acceder al valor. Las líneas 9 y 10 de la figura 3.4 muestran el valor del parámetro nombreDelCurso, mediante el uso del especificador de formato %s en la cadena de formato de printf. Observe que el nombre de la variable de parámetro (figura 3.4, línea 7) puede ser igual o distinto al nombre de la variable de argumento (figura 3.5, línea 24). Un método puede especificar múltiples parámetros; sólo hay que separar un parámetro de otro mediante una coma (en el capítulo 6 veremos un ejemplo de esto). El número de argumentos en la llamada a un método debe coincidir con el número de parámetros en la lista de parámetros de la declaración del método que se llamó. Además, los tipos de los argumentos en la llamada al método deben ser “consistentes con” los tipos de los parámetros correspondientes en la declaración del método (como veremos en capítulos posteriores, no siempre se requiere que el tipo de un argumento y el tipo de su correspondiente parámetro sean idénticos). En nuestro ejemplo, la llamada al método pasa un argumento de tipo String (nombreDelCurso se declara como String en la línea 19 de la figura 3.5) y la declaración del método especifica un parámetro de tipo String (línea 7 en la figura 3.4). Por lo tanto, en este ejemplo, el tipo del argumento en la llamada al método coincide exactamente con el tipo del parámetro en el encabezado del método.

Error común de programación 3.2 Si el número de argumentos en la llamada a un método no coincide con el número de parámetros en la declaración del método, se produce un error de compilación.

Error común de programación 3.3 Si los tipos de los argumentos en la llamada a un método no son consistentes con los tipos de los parámetros correspondientes en la declaración del método, se produce un error de compilación.

Diagrama de clases de UML actualizado para la clase LibroCalificaciones El diagrama de clases de UML de la figura 3.6 modela la clase LibroCalificaciones de la figura 3.4. Al igual que la Figura 3.1, esta clase LibroCalificaciones contiene la operación public llamada mostrarMensaje. Sin embargo, esta versión de mostrarMensaje tiene un parámetro. La forma en que UML modela un parámetro es un poco distinta a la de Java, ya que lista el nombre del parámetro, seguido de dos puntos y del tipo del parámetro entre paréntesis, después del nombre de la operación. UML tiene sus propios tipos de datos, que son similares a los de Java (pero como veremos, no todos los tipos de datos de UML tienen los mismos nombres que los tipos correspondientes en Java). El tipo String de UML corresponde al tipo String de Java. El método mostrarMensaje de LibroCalificaciones (figura 3.4) tiene un parámetro String llamado nombreDelCurso, por lo que en la figura 3.6 se lista a nombreDelCurso : String entre los paréntesis que van después de mostrarMensaje.

LibroCalificaciones

+ mostrarMensaje( nombreDelCurso : String )

Figura 3.6 | Diagrama de clases de UML, que indica que la clase LibroCalificaciones tiene una operación llamada mostrarMensaje, con un parámetro llamado nombreDelCurso de tipo String de UML.

www.elsolucionario.net

84

Capítulo 3

Introducción a las clases y los objetos

Observaciones acerca del uso de las declaraciones import Observe la declaración import en la figura 3.5 (línea 4). Esto indica al compilador que el programa utiliza la clase Scanner. ¿Por qué necesitamos importar la clase Scanner, pero no las clases System, String o LibroCalificaciones? La mayoría de las clases que utilizará en los programas de Java deben importarse. Las clases System y String están en el paquete java.lang, que se importa de manera implícita en todo programa de Java, por lo que todos los programas pueden usar las clases del paquete java.lang sin tener que importarlas de manera explícita. Hay una relación especial entre las clases que se compilan en el mismo directorio en el disco, como las clases LibroCalificaciones y PruebaLibroCalificaciones. De manera predeterminada, se considera que dichas clases se encuentran en el mismo paquete; a éste se le conoce como el paquete predeterminado. Las clases en el mismo paquete se importan implícitamente en los archivos de código fuente de las otras clases en el mismo paquete. Por ende, no se requiere una declaración import cuando la clase en un paquete utiliza a otra en el mismo paquete; como cuando PruebaLibroCalificaciones utiliza a la clase LibroCalificaciones. La declaración import en la línea 4 no es obligatoria si siempre hacemos referencia a la clase Scanner como java.util.Scanner, que incluye el nombre completo del paquete y de la clase. Esto se conoce como el nombre de clase completamente calificado. Por ejemplo, la línea 12 podría escribirse como java.util.Scanner entrada = new java.util.Scanner( System.in );

Observación de ingeniería de software 3.2 El compilador de Java no requiere declaraciones import en un archivo de código fuente de Java, si se especifica el nombre de clase completamente calificado cada vez que se utilice el nombre de una clase en el código fuente. Pero la mayoría de los programadores de Java consideran que el uso de nombres completamente calificados es incómodo, por lo cual prefieren usar declaraciones import.

3.5 Variables de instancia, métodos establecer y métodos obtener En el capítulo 2 declaramos todas las variables de una aplicación en el método main. Las variables que se declaran en el cuerpo de un método específico se conocen como variables locales, y sólo se pueden utilizar en ese método. Cuando termina ese método, se pierden los valores de sus variables locales. En la sección 3.2 vimos que un objeto tiene atributos que lleva consigo cuando se utiliza en un programa. Dichos atributos existen antes de que un objeto llame a un método, y después de que el método completa su ejecución. Por lo general, una clase consiste en uno o más métodos que manipulan los atributos pertenecientes a un objeto específico de la clase. Los atributos se representan como variables en la declaración de la clase. Dichas variables se llaman campos, y se declaran dentro de la declaración de una clase, pero fuera de los cuerpos de las declaraciones de los métodos de la clase. Cuando cada objeto de una clase mantiene su propia copia de un atributo, el campo que representa a ese atributo se conoce también como variable de instancia; cada objeto (instancia) de la clase tiene una instancia separada de la variable en memoria. El ejemplo en esta sección demuestra una clase LibroCalificaciones, que contiene una variable de instancia llamada nombreDelCurso para representar el nombre del curso de un objeto LibroCalificaciones específico.

La clase LibroCalificaciones con una variable de instancia, un método establecer y un método obtener En nuestra siguiente aplicación (figuras 3.7 y 3.8), la clase LibroCalificaciones (figura 3.7) mantiene el nombre del curso como una variable de instancia, para que pueda usarse o modificarse en cualquier momento, durante la ejecución de una aplicación. Esta clase contiene tres métodos: establecerNombreDelCurso, obtenerNombreDelCurso y mostrarMensaje. El método establecerNombreDelCurso almacena el nombre de un curso en un LibroCalificaciones. El método obtenerNombreDelCurso obtiene el nombre del curso de un LibroCalificaciones. El método mostrarMensaje, que en este caso no especifica parámetros, sigue mostrando un mensaje de bienvenida que incluye el nombre del curso; como veremos más adelante, el método ahora obtiene el nombre del curso mediante una llamada a otro método en la misma clase: obtenerNombreDelCurso. Por lo general, un instructor enseña más de un curso, cada uno con su propio nombre. La línea 7 declara que nombreDelCurso es una variable de tipo String. Como la variable se declara en el cuerpo de la clase, pero fuera de los cuerpos de los métodos de la misma (líneas 10 a la 13, 16 a la 19 y 22 a la 28), la línea 7 es una declaración para una variable de instancia. Cada instancia (es decir, objeto) de la clase LibroCalificaciones contiene una

www.elsolucionario.net

3.5

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

Variables de instancia, métodos establecer y métodos obtener

85

// Fig. 3.7: LibroCalificaciones.java // Clase LibroCalificaciones que contiene una variable de instancia nombreDelCurso // y métodos para establecer y obtener su valor. public class LibroCalificaciones { private String nombreDelCurso; // nombre del curso para este LibroCalificaciones // método para establecer el nombre del curso public void establecerNombreDelCurso( String nombre ) { nombreDelCurso = nombre; // almacena el nombre del curso } // fin del método establecerNombreDelCurso // método para obtener el nombre del curso public String obtenerNombreDelCurso() { return nombreDelCurso; } // fin del método obtenerNombreDelCurso // muestra un mensaje de bienvenida al usuario de LibroCalificaciones public void mostrarMensaje() { // esta instrucción llama a obtenerNombreDelCurso para obtener el // nombre del curso que representa este LibroCalificaciones System.out.printf( “Bienvenido al libro de calificaciones para\n%s!\n”, obtenerNombreDelCurso() ); } // fin del método mostrarMensaje } // fin de la clase LibroCalificaciones

Figura 3.7 | Clase LibroCalificaciones que contiene una variable de instancia nombreDelCurso y métodos para establecer y obtener su valor. copia de cada variable de instancia. Por ejemplo, si hay dos objetos LibroCalificaciones, cada objeto tiene su propia copia de nombreDelCurso (una por cada objeto). Un beneficio de hacer de nombreDelCurso una variable de instancia es que todos los métodos de la clase (en este caso, LibroCalificaciones) pueden manipular cualquier variable de instancia que aparezca en la clase (en este caso, nombreDelCurso).

Los modificadores de acceso public y private La mayoría de las declaraciones de variables de instancia van precedidas por la palabra clave private (como en la línea 7). Al igual que public, la palabra clave private es un modificador de acceso. Las variables o los métodos declarados con el modificador de acceso private son accesibles sólo para los métodos de la clase en la que se declaran. Así, la variable nombreDelCurso sólo puede utilizarse en los métodos establecerNombreDelCurso, obtenerNombreDelCurso y mostrarMensaje de (cada objeto de) la clase LibroCalificaciones.

Observación de ingeniería de software 3.3 Es necesario colocar un modificador de acceso antes de cada declaración de un campo y de un método. Como regla empírica, las variables de instancia deben declararse como private y los métodos, como public. (Más adelante veremos que es apropiado declarar ciertos métodos como private, si sólo van a estar accesibles por otros métodos de la clase).

Buena práctica de programación 3.1 Preferimos listar los campos de una clase primero, para que, a medida que usted lea el código, pueda ver los nombres y tipos de las variables antes de ver su uso en los métodos de la clase. Es posible listar los campos de la clase en cualquier parte de la misma, fuera de las declaraciones de sus métodos, pero si se esparcen por todo el código, éste será más difícil de leer.

www.elsolucionario.net

86

Capítulo 3

Introducción a las clases y los objetos

Buena práctica de programación 3.2 Coloque una línea en blanco entre las declaraciones de los métodos, para separarlos y mejorar la legibilidad del programa.

El proceso de declarar variables de instancia con el modificador de acceso private se conoce como ocultamiento de datos. Cuando un programa crea (instancia) un objeto de la clase LibroCalificaciones, la variable nombreDelCurso se encapsula (oculta) en el objeto, y sólo está accesible para los métodos de la clase de ese objeto. En la clase LibroCalificaciones, los métodos establecerNombreDelCurso y obtenerNombreDelCurso manipulan a la variable de instancia nombreDelCurso. El método establecerNombreDelCurso (líneas 10 a la 13) no devuelve datos cuando completa su tarea, por lo que su tipo de valor de retorno es void. El método recibe un parámetro (nombre), el cual representa el nombre del curso que se pasará al método como un argumento. La línea 12 asigna nombre a la variable de instancia nombreDelCurso. El método obtenerNombreDelCurso (líneas 16 a la 19) devuelve un nombreDelCurso de un objeto LibroCalificaciones específico. El método tiene una lista de parámetros vacía, por lo que no requiere información adicional para realizar su tarea. El método especifica que devuelve un objeto String; a éste se le conoce como el tipo de valor de retorno del método. Cuando se hace una llamada a un método que especifica un tipo de valor de retorno, y éste completa su tarea, devuelve un resultado al método que lo llamó. Por ejemplo, cuando usted va a un cajero automático (ATM) y solicita el saldo de su cuenta, espera que el ATM le devuelva un valor que representa su saldo. De manera similar, cuando una instrucción llama al método obtenerNombreDelCurso en un objeto LibroCalificaciones, la instrucción espera recibir el nombre del curso de LibroCalificaciones (en este caso, un objeto String, como se especifica en el tipo de valor de retorno de la declaración del método). Si tiene un método cuadrado que devuelve el cuadrado de su argumento, es de esperarse que la instrucción int resultado = cuadrado( 2 );

devuelva 4 del método cuadrado y asigne 4 a la variable resultado. Si tiene un método maximo que devuelve el mayor de tres argumentos enteros, es de esperarse que la siguiente instrucción int mayor = maximo( 27, 114, 51 );

devuelva 114 del método maximo y asigne 114 a la variable mayor. Observe que cada una de las instrucciones en las líneas 12 y 18 utilizan nombreDelCurso, aun cuando esta variable no se declaró en ninguno de los métodos. Podemos utilizar nombreDelCurso en los métodos de la clase LibroCalificaciones, ya que nombreDelCurso es un campo de la clase. Observe además que el orden en el que se declaran los métodos en una clase no determina cuándo se van a llamar en tiempo de ejecución. Por lo tanto, el método obtenerNombreDelCurso podría declararse antes del método establecerNombreDelCurso. El método mostrarMensaje (líneas 22 a la 28) no devuelve datos cuando completa su tarea, por lo que su tipo de valor de retorno es void. El método no recibe parámetros, por lo que la lista de parámetros está vacía. Las líneas 26 y 27 imprimen un mensaje de bienvenida, que incluye el valor de la variable de instancia nombreDelCurso. Una vez más, necesitamos crear un objeto de la clase LibroCalificaciones y llamar a sus métodos para poder mostrar en pantalla el mensaje de bienvenida.

La clase PruebaLibroCalificaciones que demuestra a la clase LibroCalificaciones La clase PruebaLibroCalificaciones (figura 3.8) crea un objeto de la clase LibroCalificaciones y demuestra el uso de sus métodos. La línea 11 crea un objeto Scanner, que se utilizará para obtener el nombre de un curso del usuario. La línea 14 crea un objeto LibroCalificaciones y lo asigna a la variable local miLibroCalificaciones, de tipo LibroCalificaciones. Las líneas 17-18 muestran el nombre inicial del curso mediante una llamada al método obtenerNombreDelCurso del objeto. Observe que la primera línea de la salida muestra el nombre “null”. A diferencia de las variables locales, que no se inicializan de manera automática, cada campo tiene un valor inicial predeterminado: un valor que Java proporciona cuando el programador no especifica el valor inicial del campo. Por ende, no se requiere que los campos se inicialicen explícitamente antes de usarlos en un programa, a menos que deban inicializarse con valores distintos de los predeterminados. El valor predeterminado para un campo de tipo String (como nombreDelCurso en este ejemplo) es null, de lo cual hablaremos con más detalle en la sección 3.6.

www.elsolucionario.net

3.5

Variables de instancia, métodos establecer y métodos obtener

87

La línea 21 pide al usuario que escriba el nombre para el curso. La variable String local elNombre (declarada en la línea 22) se inicializa con el nombre del curso que escribió el usuario, el cual se devuelve mediante la llamada al método nextLine del objeto Scanner llamado entrada. La línea 23 llama al método establecerNombreDelCurso del objeto miLibroCalificaciones y provee elNombre como argumento para el método. Cuando se hace la llamada al método, el valor del argumento se asigna al parámetro nombre (línea 10, figura 3.7) del método establecerNombreDelCurso (líneas 10 a la 13, figura 3.7). Después, el valor del parámetro se asigna a la variable de instancia nombreDelCurso (línea 12, figura 3.7). La línea 24 (figura 3.8) salta una línea en la salida, y después la línea 27 llama al método mostrarMensaje del objeto miLibroCalificaciones para mostrar en pantalla el mensaje de bienvenida, que contiene el nombre del curso.

Los métodos establecer y obtener Los campos private de una clase pueden manipularse sólo mediante los métodos de esa clase. Por lo tanto, un cliente de un objeto (es decir, cualquier clase que llame a los métodos del objeto) llama a los métodos public de la clase para manipular los campos private de un objeto de esa clase. Esto explica por qué las instrucciones en el método main (figura 3.8) llaman a los métodos establecerNombreDelCurso, obtenerNombreDelCurso y mostrarMensaje en un objeto LibroCalificaciones. A menudo, las clases proporcionan métodos public para permitir a los clientes de la clase establecer (es decir, asignar valores a) u obtener (es decir, obtener los valores de) variables de instancia private. Los nombres de estos métodos no necesitan empezar con establecer u obtener, pero esta convención de nomenclatura es muy recomendada en Java, y es requerida para ciertos componentes de software especiales de Java, conocidos como JavaBeans, que pueden simplificar la programación en muchos entornos de desarrollo integrados (IDEs). El método que establece la variable nombreDelCurso en este ejemplo se llama establecerNombreDelCurso, y el método que obtiene el valor de la variable de instancia nombreDelCurso se llama obtenerNombreDelCurso.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

// Fig. 3.8: PruebaLibroCalificaciones.java // Crea y manipula un objeto LibroCalificaciones. import java.util.Scanner; // el programa usa la clase Scanner public class PruebaLibroCalificaciones { // el método main empieza la ejecución del programa public static void main( String args[] ) { // crea un objeto Scanner para obtener la entrada de la ventana de comandos Scanner entrada = new Scanner( System.in ); // crea un objeto LibroCalificaciones y lo asigna a miLibroCalificaciones LibroCalificaciones miLibroCalificaciones = new LibroCalificaciones(); // muestra el valor inicial de nombreDelCurso System.out.printf( “El nombre inicial del curso es: %s\n\n”, miLibroCalificaciones.obtenerNombreDelCurso() ); // pide y lee el nombre del curso System.out.println( “Escriba el nombre del curso:” ); String elNombre = entrada.nextLine(); // lee una línea de texto miLibroCalificaciones.establecerNombreDelCurso( elNombre ); // establece el nombre del curso System.out.println(); // imprime una línea en blanco // muestra el mensaje de bienvenida después de especificar el nombre del curso miLibroCalificaciones.mostrarMensaje(); } // fin de main } // fin de la clase PruebaLibroCalificaciones

Figura 3.8 | Creación y manipulación de un objeto LibroCalificaciones. (Parte 1 de 2).

www.elsolucionario.net

88

Capítulo 3

Introducción a las clases y los objetos

El nombre inicial del curso es: null Escriba el nombre del curso: CS101 Introduccion a la programacion en Java Bienvenido al libro de calificaciones para CS101 Introduccion a la programacion en Java!

Figura 3.8 | Creación y manipulación de un objeto LibroCalificaciones. (Parte 2 de 2).

Diagrama de clases de UML para la clase LibroCalificaciones con una variable de instancia, y métodos establecer y obtener La figura 3.9 contiene un diagrama de clases de UML actualizado para la versión de la clase LibroCalificaciones de la figura 3.7. Este diagrama modela la variable de instancia nombreDelCurso de la clase LibroCalificaciones como un atributo en el compartimiento intermedio de la clase. UML representa a las variables de instancia como atributos, listando el nombre del atributo, seguido de dos puntos y del tipo del atributo. El tipo de UML del atributo nombreDelCurso es String. La variable de instancia nombreDelCurso es private en Java, por lo que el diagrama de clases lista un signo menos (-) en frente del nombre del atributo correspondiente. La clase LibroCalificaciones contiene tres métodos public, por lo que el diagrama de clases lista tres operaciones en el tercer compartimiento. Recuerde que el signo más (+) antes de cada nombre de operación indica que ésta es public. La operación establecerNombreDelCurso tiene un parámetro String llamado nombre. UML indica el tipo de valor de retorno de una operación colocando dos puntos y el tipo de valor de retorno después de los paréntesis que le siguen al nombre de la operación. El método obtenerNombreDelCurso de la clase LibroCalificaciones (figura 3.7) tiene un tipo de valor de retorno String en Java, por lo que el diagrama de clases muestra un tipo de valor de retorno String en UML. Observe que las operaciones establecerNombreDelCurso y mostrarMensaje no devuelven valores (es decir, devuelven void en Java), por lo que el diagrama de clases de UML no especifica un tipo de valor de retorno después de los paréntesis de estas operaciones.

LibroCalificaciones – nombreDelCurso : String + establecerNombreDelCurso( nombre : String ) + obtenerNombreDelCurso( ) : String + mostrarMensaje( )

Figura 3.9 | Diagrama de clases de UML, en el que se indica que la clase LibroCalificaciones tiene un atributo nombreDelCurso de tipo String en UML, y tres operaciones: establecerNombreDelCurso (con un parámetro nombre de tipo String de UML), obtenerNombreDelCurso (que devuelve el tipo String de UML) y mostrarMensaje.

3.6 Comparación entre tipos primitivos y tipos por referencia Los tipos de datos en Java se dividen en dos categorías: tipos primitivos y tipos por referencia (algunas veces conocidos como tipos no primitivos). Los tipos primitivos son boolean, byte, char, short, int, long, float y double. Todos los tipos no primitivos son tipos por referencia, por lo cual las clases, que especifican los tipos de objetos, son tipos por referencia. Una variable de tipo primitivo puede almacenar sólo un valor de su tipo declarado a la vez. Por ejemplo, una variable int puede almacenar un número completo (como 7) a la vez. Cuando se asigna otro valor a esa variable, se sustituye su valor inicial. Las variables de instancia de tipo primitivo se inicializan de manera predeterminada; las variables de los tipos byte, char, short, int, long, float y double se inicializan con 0, y las variables de tipo boolean se inicializan con false. Usted puede especificar sus propios valores iniciales para las variables de tipo primitivo. Recuerde que las variables locales no se inicializan de manera predeterminada.

www.elsolucionario.net

3.7

Inicialización de objetos mediante constructores

89

Tip para prevenir errores 3.1 Cualquier intento de utilizar una variable local que no se haya inicializado produce un error de compilación.

Los programas utilizan variables de tipo por referencia (que por lo general se llaman referencias) para almacenar las ubicaciones de los objetos en la memoria de la computadora. Se dice que dicha variable hace referencia a un objeto en el programa. Cada uno de los objetos a los que se hace referencia pueden contener muchas variables de instancia y métodos. La línea 14 de la figura 3.8 crea un objeto de la clase LibroCalificaciones, y la variable miLibroCalificaciones contiene una referencia a ese objeto LibroCalificaciones. Las variables de instancia de tipo por referencia se inicializan de manera predeterminada con el valor null: una palabra reservada que representa una “referencia a nada”. Esto explica por qué la primera llamada a obtenerNombreDelCurso en la línea 18 de la figura 3.8 devolvía null; no se había establecido el valor de nombreDelCurso, por lo que se devolvía el valor inicial predeterminado null. En el apéndice C, Palabras clave y palabras reservadas, se muestra una lista completa de las palabras reservadas y las palabras clave. Es obligatorio que una referencia a un objeto invoque (es decir, llame) a los métodos de un objeto. En la aplicación de la figura 3.8, las instrucciones en el método main utilizan la variable miLibroCalificaciones para enviar mensajes al objeto LibroCalificaciones. Estos mensajes son llamadas a métodos (como establecerNombreDelCurso y obtenerNombreDelCurso) que permiten al programa interactuar con el objeto LibroCalificaciones. Por ejemplo, la instrucción en la línea 23 utiliza a miLibroCalificaciones para enviar el mensaje establecerNombreDelCurso al objeto LibroCalificaciones. El mensaje incluye el argumento que requiere establecerNombreDelCurso para realizar su tarea. El objeto LibroCalificaciones utiliza esta información para establecer la variable de instancia nombreDelCurso. Tenga en cuenta que las variables de tipo primitivo no hacen referencias a objetos, por lo que dichas variables no pueden utilizarse para invocar métodos.

Observación de ingeniería de software 3.4 El tipo declarado de una variable (por ejemplo, int, double o LibroCalificaciones) indica si la variable es de tipo primitivo o por referencia. Si el tipo de una variable no es uno de los ocho tipos primitivos, entonces es un tipo por referencia. (Por ejemplo, Cuenta cuenta1 indica que cuenta1 es una referencia a un objeto Cuenta).

3.7 Inicialización de objetos mediante constructores Como mencionamos en la sección 3.5, cuando se crea un objeto de la clase LibroCalificaciones (figura 3.7), su variable de instancia nombreCurso se inicializa con null de manera predeterminada. ¿Qué pasa si usted desea proporcionar el nombre de un curso a la hora de crear un objeto LibroCalificaciones? Cada clase que usted declare puede proporcionar un constructor, el cual puede utilizarse para inicializar un objeto de una clase al momento de crear ese objeto. De hecho, Java requiere una llamada al constructor para cada objeto que se crea. La palabra clave new llama al constructor de la clase para realizar la inicialización. La llamada al constructor se indica mediante el nombre de la clase, seguido de paréntesis; el constructor debe tener el mismo nombre que la clase. Por ejemplo, la línea 14 de la figura 3.8 primero utiliza new para crear un objeto LibroCalificaciones. Los paréntesis vacíos después de "new LibroCalificaciones" indican una llamada sin argumentos al constructor de la clase. De manera predeterminada, el compilador proporciona un constructor predeterminado sin parámetros, en cualquier clase que no incluya un constructor en forma explícita. Cuando una clase sólo tiene el constructor predeterminado, sus variables de instancia se inicializan con sus valores predeterminados. Las variables de los tipos char, byte, short, int, long, float y double se inicializan con 0, las variables de tipo boolean se inicializan con false, y las variables de tipo por referencia se inicializan con null. Cuando usted declara una clase, puede proporcionar su propio constructor para especificar una inicialización personalizada para los objetos de su clase. Por ejemplo, tal vez un programador quiera especificar el nombre de un curso para un objeto LibroCalificaciones cuando se crea este objeto, como en LibroCalificaciones miLibroCalificaciones = new LibroCalificaciones( "CS101 Introduccion a la programacion en Java" );

En este caso, el argumento "CS101 Introduccion a la programacion en Java" se pasa al constructor del objeto LibroCalificaciones y se utiliza para inicializar el nombreDelCurso. La instrucción anterior requiere que la clase proporcione un constructor con un parámetro String. La figura 3.10 contiene una clase LibroCalificaciones modificada con dicho constructor.

www.elsolucionario.net

90

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

Capítulo 3

Introducción a las clases y los objetos

// Fig. 3.10: LibroCalificaciones.java // La clase LibroCalificaciones con un constructor para inicializar el nombre del curso. public class LibroCalificaciones { private String nombreDelCurso; // nombre del curso para este LibroCalificaciones // el constructor inicializa nombreDelCurso con el objeto String que se provee como argumento public LibroCalificaciones( String nombre ) { nombreDelCurso = nombre; // inicializa nombreDelCurso } // fin del constructor // método para establecer el nombre del curso public void establecerNombreDelCurso( String nombre ) { nombreDelCurso = nombre; // almacena el nombre del curso } // fin del método establecerNombreDelCurso // método para obtener el nombre del curso public String obtenerNombreDelCurso() { return nombreDelCurso; } // fin del método obtenerNombreDelCurso // muestra un mensaje de bienvenida al usuario de LibroCalificaciones public void mostrarMensaje() { // esta instrucción llama a obtenerNombreDelCurso para obtener el // nombre del curso que este LibroCalificaciones representa System.out.printf( “Bienvenido al Libro de calificaciones para\n%s!\n”, obtenerNombreDelCurso() ); } // fin del método mostrarMensaje } // fin de la clase LibroCalificaciones

Figura 3.10 | La clase LibroCalificaciones con un constructor para inicializar el nombre del curso.

Las líneas 9 a la 12 declaran el constructor para la clase LibroCalificaciones. Un constructor debe tener el mismo nombre que su clase. Al igual que un método, un constructor especifica en su lista de parámetros los datos que requiere para realizar su tarea. Cuando usted crea un nuevo objeto (como haremos en la figura 3.11), estos datos se colocan en los paréntesis que van después del nombre de la clase. La línea 9 indica que el constructor de la clase LibroCalificaciones tiene un parámetro String llamado nombre. El nombre que se pasa al constructor se asigna a la variable de instancia nombreCurso en la línea 11 del cuerpo del constructor. La figura 3.11 demuestra la inicialización de objetos LibroCalificaciones mediante el uso de este constructor. Las líneas 11 y 12 crean e inicializan el objeto libroCalificaciones1 de LibroCalificaciones. El constructor de la clase LibroCalificaciones se llama con el argumento "CS101 Introduccion a la programacion en Java" para inicializar el nombre del curso. La expresión de creación de la instancia de la clase a la derecha del signo = en las líneas 11 y 12 devuelve una referencia al nuevo objeto, el cual se asigna a la variable libroCalificaciones1. Las líneas 13 y 14 repiten este proceso para otro objeto LibroCalificaciones llamado libroCalificaciones2, pero esta vez se le pasa el argumento "CS102 Estructuras de datos en Java" para inicializar el nombre del curso para libroCalificaciones2. Las líneas 17 a la 20 utilizan el método obtenerNombreDelCurso de cada objeto para obtener los nombres de los cursos y mostrar que, sin duda, se inicializaron en el momento en el que se crearon los objetos. En la introducción a la sección 3.5, aprendió que cada instancia (es decir, objeto) de una clase contiene su propia copia de las variables de instancia de la clase. La salida confirma que cada objeto LibroCalificaciones mantiene su propia copia de la variable de instancia nombreCurso.

www.elsolucionario.net

3.8

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

Números de punto flotante y el tipo double

91

// Fig. 3.11: PruebaLibroCalificaciones.java // El constructor de LibroCalificaciones se utiliza para especificar el // nombre del curso cada vez que se crea cada objeto LibroCalificaciones. public class PruebaLibroCalificaciones { // el método main empieza la ejecución del programa public static void main( String args[] ) { // crea objeto LibroCalificaciones LibroCalificaciones libroCalificaciones1 = new LibroCalificaciones( “CS101 Introduccion a la programacion en Java” ); LibroCalificaciones libroCalificaciones2 = new LibroCalificaciones( “CS102 Estructuras de datos en Java” ); // muestra el valor inicial de nombreDelCurso para cada LibroCalificaciones System.out.printf( “El nombre del curso de libroCalificaciones1 es: %s\n”, libroCalificaciones1.obtenerNombreDelCurso() ); System.out.printf( “El nombre del curso de libroCalificaciones2 es: %s\n”, libroCalificaciones2.obtenerNombreDelCurso() ); } // fin de main } // fin de la clase PruebaLibroCalificaciones

El nombre del curso de libroCalificaciones1 es: CS101 Introduccion a la programacion en Java El nombre del curso de libroCalificaciones2 es: CS102 Estructuras de datos en Java

Figura 3.11 | El constructor de LibroCalificaciones se utiliza para especificar el nombre del curso cada vez que se crea un objeto LibroCalificaciones.

Al igual que los métodos, los constructores también pueden recibir argumentos. No obstante, una importante diferencia entre los constructores y los métodos es que los constructores no pueden devolver valores, por lo cual no pueden especificar un tipo de valor de retorno (ni siquiera void). Por lo general, los constructores se declaran como public. Si una clase no incluye un constructor, las variables de instancia de esa clase se inicializan con sus valores predeterminados. Si un programador declara uno o más constructores para una clase, el compilador de Java no creará un constructor predeterminado para esa clase.

Tip para prevenir errores 3.2 A menos que sea aceptable la inicialización predeterminada de las variables de instancia de su clase, deberá proporcionar un constructor para asegurarse que las variables de instancia de su clase se inicialicen en forma apropiada con valores significativos, a la hora de crear cada nuevo objeto de su clase.

Agregar el constructor al diagrama de clases de UML de la clase LibroCalificaciones El diagrama de clases de UML de la figura 3.12 modela la clase LibroCalificaciones de la figura 3.10, la cual tiene un constructor con un parámetro llamado nombre, de tipo String. Al igual que las operaciones, en un diagrama de clases, UML modela a los constructores en el tercer compartimiento de una clase. Para diferenciar a un constructor de las operaciones de una clase, UML requiere que se coloque la palabra “constructor” entre los signos « y » antes del nombre del constructor. Es costumbre listar los constructores antes de otras operaciones en el tercer compartimiento.

3.8 Números de punto flotante y el tipo double

En nuestra siguiente aplicación, dejaremos por un momento nuestro ejemplo práctico con la clase LibroCalificaciones para declarar una clase llamada Cuenta, la cual mantiene el saldo de una cuenta bancaria. La mayoría de los saldos de las cuentas no son números enteros (por ejemplo, 0, –22 y 1024). Por esta razón, la clase Cuenta

www.elsolucionario.net

92

Capítulo 3

Introducción a las clases y los objetos

LibroCalificaciones – nombreDelCurso : String «constructor» LibroCalificaciones( nombre : String ) + establecerNombreDelCurso( nombre : String ) + obtenerNombreDelCurso( ) : String + mostrarMensaje( )

Figura 3.12 | Diagrama de clases de UML, en el cual se indica que la clase LibroCalificaciones tiene un constructor con un parámetro nombre del tipo String de UML.

representa el saldo de las cuentas como un número de punto flotante (es decir, un número con un punto decimal, como 7.33, 0.0975 o 1000.12345). Java cuenta con dos tipos primitivos para almacenar números de punto flotante en la memoria: float y double. La principal diferencia entre ellos es que las variables tipo double pueden almacenar números con mayor magnitud y detalle (es decir, más dígitos a la derecha del punto decimal; lo que también se conoce como precisión del número) que las variables float.

Precisión de los números de punto flotante y requerimientos de memoria Las variables de tipo float representan números de punto flotante de precisión simple y tienen siete dígitos significativos. Las variables de tipo double representan números de punto flotante de precisión doble. Éstos requieren el doble de memoria que las variables float y proporcionan 15 dígitos significativos; aproximadamente el doble de precisión de las variables float. Para el rango de valores requeridos por la mayoría de los programas, debe bastar con las variables de tipo float, pero podemos utilizar variables tipo double para “ir a la segura”. En algunas aplicaciones, incluso hasta las variables de tipo double serán inadecuadas; dichas aplicaciones se encuentran más allá del alcance de este libro. La mayoría de los programadores representan los números de punto flotante con el tipo double. De hecho, Java trata a todos los números de punto flotante que escribimos en el código fuente de un programa (como 7.33 y 0.0975) como valores double de manera predeterminada. Dichos valores en el código fuente se conocen como literales de punto flotante. En el apéndice D, Tipos primitivos, podrá consultar los rangos de los valores para los tipos float y double. Aunque los números de punto flotante no son siempre 100% precisos, tienen numerosas aplicaciones. Por ejemplo, cuando hablamos de una temperatura corporal “normal” de 36.8, no necesitamos una precisión con un número extenso de dígitos. Cuando leemos la temperatura en un termómetro como 36.8, en realidad podría ser 36.7999473210643. Si consideramos a este número simplemente como 36.8, está bien para la mayoría de las aplicaciones en las que se trabaja con las temperaturas corporales. Debido a la naturaleza imprecisa de los números de punto flotante, se prefiere el tipo double al tipo float ya que las variables double pueden representar números de punto flotante con más precisión. Por esta razón, utilizaremos el tipo double a lo largo de este libro. Los números de punto flotante también surgen como resultado de la división. En la aritmética convencional, cuando dividimos 10 entre 3 el resultado es 3.3333333…, y la secuencia de números 3 se repite en forma indefinida. La computadora asigna sólo una cantidad fija de espacio para almacenar un valor de este tipo, por lo que, sin duda, el valor de punto flotante almacenado sólo puede ser una aproximación.

Error común de programación 3.4 El uso de números de punto flotante en una forma en la que se asuma que se representan con precisión puede producir errores lógicos.

La clase Cuenta con una variable de instancia de tipo double Nuestra siguiente aplicación (figuras 3.13 y 3.14) contiene una clase llamada Cuenta (figura 3.13), la cual mantiene el saldo de una cuenta bancaria. Un banco ordinario da servicio a muchas cuentas, cada una con su propio saldo, por lo que la línea 7 declara una variable de instancia, de tipo double, llamada saldo . La variable saldo es una variable de instancia, ya que está declarada en el cuerpo de la clase pero fuera de las declaraciones de los métodos de la misma (líneas 10 a la 16, 19 a la 22 y 25 a la 28). Cada instancia (es decir, objeto) de la clase Cuenta contiene su propia copia de saldo.

www.elsolucionario.net

3.8

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

Números de punto flotante y el tipo double

93

// Fig. 3.13: Cuenta.java // La clase Cuenta con un constructor para // inicializar la variable de instancia saldo. public class Cuenta { private double saldo; // variable de instancia que almacena el saldo // constructor public Cuenta( double saldoInicial ) { // valida que saldoInicial sea mayor que 0.0; // si no lo es, saldo se inicializa con el valor predeterminado 0.0 if ( saldoInicial > 0.0 ) saldo = saldoInicial; } // fin del constructor de Cuenta // abona (suma) un monto a la cuenta public void abonar( double monto ) { saldo = saldo + monto; // suma el monto al saldo } // fin del método abonar // devuelve el saldo de la cuenta public double obtenerSaldo() { return saldo; // proporciona el valor de saldo al método que hizo la llamada } // fin del método obtenerSaldo } // fin de la clase Cuenta

Figura 3.13 | La clase Cuenta con una variable de instancia de tipo double.

La clase Cuenta contiene un constructor y dos métodos. Debido a que es común que alguien abra una cuenta para depositar dinero de inmediato, el constructor (líneas 10 a la 16) recibe un parámetro llamado saldoInicial de tipo double, el cual representa el saldo inicial de la cuenta. Las líneas 14 y 15 aseguran que saldoInicial sea mayor que 0.0. De ser así, el valor de saldoInicial se asigna a la variable de instancia saldo. En caso contrario, saldo permanece en 0.0, su valor inicial predeterminado. El método abonar (líneas 19 a la 22) no devuelve datos cuando completa su tarea, por lo que su tipo de valor de retorno es void. El método recibe un parámetro llamado monto: un valor double que se sumará al saldo. La línea 21 suma monto al valor actual de saldo, y después asigna el resultado a saldo (con lo cual se sustituye el monto del saldo anterior). El método obtenerSaldo (líneas 25 a la 28) permite a los clientes de la clase (es decir, otras clases que utilicen esta clase) obtener el valor del saldo de un objeto Cuenta específico. El método especifica el tipo de valor de retorno double y una lista de parámetros vacía. Observe una vez más que las instrucciones en las líneas 15, 21 y 27 utilizan la variable de instancia saldo, aun y cuando no se declaró en ninguno de los métodos. Podemos usar saldo en estos métodos, ya que es una variable de instancia de la clase.

La clase PruebaCuenta que utiliza a la clase Cuenta La clase PruebaCuenta (figura 3.14) crea dos objetos Cuenta (líneas 10 y 11) y los inicializa con 50.00 y -7.53, respectivamente. Las líneas 14 a la 17 imprimen el saldo en cada objeto Cuenta mediante una llamada al método obtenerSaldo de Cuenta. Cuando se hace una llamada al método obtenerSaldo para cuenta1 en la línea 15, se devuelve el valor del saldo de cuenta1 de la línea 27 en la figura 3.13, y se imprime en pantalla mediante la instrucción System.out.printf (figura 3.14, líneas 14 y 15). De manera similar, cuando se hace la llamada al método obtenerSaldo para cuenta2 en la línea 17, se devuelve el valor del saldo de cuenta2 de la línea 27 en la

www.elsolucionario.net

94

Capítulo 3

Introducción a las clases y los objetos

figura 3.13, y se imprime en pantalla mediante la instrucción System.out.printf (figura 3.14, líneas 16 y 17). Observe que el saldo de cuenta2 es 0.00, ya que el constructor se aseguró de que la cuenta no pudiera empezar con un saldo negativo. El valor se imprime en pantalla mediante printf, con el especificador de formato %.2f. El especificador de formato %f se utiliza para imprimir valores de tipo float o double. El .2 entre % y f representa el número de lugares decimales (2) que deben imprimirse a la derecha del punto decimal en el número de punto flotante; a esto también se le conoce como la precisión del número. Cualquier valor de punto flotante que se imprima con %.2f se redondeará a la posición de las centenas; por ejemplo, 123.457 se redondearía a 123.46, y 27.333 se redondearía a 27.33.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

// Fig. 3.14: PruebaCuenta.java // Entrada y salida de números de punto flotante con objetos Cuenta. import java.util.Scanner; public class PruebaCuenta { // el método main empieza la ejecución de la aplicación de Java public static void main( String args[] ) { Cuenta cuenta1 = new Cuenta( 50.00 ); // crea objeto Cuenta Cuenta cuenta2 = new Cuenta( -7.53 ); // crea objeto Cuenta // muestra el saldo inicial de cada objeto System.out.printf( “Saldo de cuenta1: $%.2f\n”, cuenta1.obtenerSaldo() ); System.out.printf( “Saldo de cuenta2: $%.2f\n\n”, cuenta2.obtenerSaldo() ); // crea objeto Scanner para obtener la entrada de la ventana de comandos Scanner entrada = new Scanner( System.in ); double montoDeposito; // deposita el monto escrito por el usuario System.out.print( “Escriba el monto a depositar para cuenta1: “ ); // indicador montoDeposito = entrada.nextDouble(); // obtiene entrada del usuario System.out.printf( “\nsumando %.2f al saldo de cuenta1\n\n”, montoDeposito ); cuenta1.abonar( montoDeposito ); // suma al saldo de cuenta1 // muestra los saldos System.out.printf( “Saldo cuenta1.obtenerSaldo() System.out.printf( “Saldo cuenta2.obtenerSaldo()

de cuenta1: $%.2f\n”, ); de cuenta2: $%.2f\n\n”, );

System.out.print( “Escriba el monto a depositar para cuenta2: “ ); // indicador montoDeposito = entrada.nextDouble(); // obtiene entrada del usuario System.out.printf( “\nsumando %.2f al saldo de cuenta2\n\n”, montoDeposito ); cuenta2.abonar( montoDeposito ); // suma al saldo de cuenta2 // muestra los saldos System.out.printf( “Saldo cuenta1.obtenerSaldo() System.out.printf( “Saldo cuenta2.obtenerSaldo() } // fin de main

de cuenta1: $%.2f\n”, ); de cuenta2: $%.2f\n”, );

} // fin de la clase PruebaCuenta

Figura 3.14 | Entrada y salida de números de punto flotante con objetos Cuenta. (Parte 1 de 2).

www.elsolucionario.net

3.9

(Opcional) Ejemplo práctico de GUI y gráficos: uso de cuadros de diálogo

95

Saldo de cuenta1: $50.00 Saldo de cuenta2: $0.00 Escriba el monto a depositar para cuenta1: 25.53 sumando 25.53 al saldo de cuenta1 Saldo de cuenta1: $75.53 Saldo de cuenta2: $0.00 Escriba el monto a depositar para cuenta2: 123.45 sumando 123.45 al saldo de cuenta2 Saldo de cuenta1: $75.53 Saldo de cuenta2: $123.45

Figura 3.14 | Entrada y salida de números de punto flotante con objetos Cuenta. (Parte 2 de 2). La línea 20 crea un objeto Scanner, el cual se utilizará para obtener montos de depósito de un usuario. La línea 21 declara la variable local montoDeposito para almacenar cada monto de depósito introducido por el usuario. A diferencia de la variable de instancia saldo en la clase Cuenta, la variable local montoDeposito en main no se inicializa con 0.0 de manera predeterminada. Sin embargo, esta variable no necesita inicializarse aquí, ya que su valor se determinará con base a la entrada del usuario. La línea 23 pide al usuario que escriba un monto a depositar para cuenta1. La línea 24 obtiene la entrada del usuario, llamando al método nextDouble del objeto Scanner llamado entrada, el cual devuelve un valor double introducido por el usuario. Las líneas 25 y 26 muestran el monto del depósito. La línea 27 llama al método abonar del objeto cuenta1 y le suministra montoDeposito como argumento. Cuando se hace la llamada al método, el valor del argumento se asigna al parámetro monto (línea 19 de la figura 3.13) del método abonar (líneas 19 a la 22 de la figura 3.13), y después el método abonar suma ese valor al saldo (línea 21 de la figura 3.13). Las líneas 30 a la 33 (figura 3.14) imprimen en pantalla los saldos de ambos objetos Cuenta otra vez, para mostrar que sólo se modificó el saldo de cuenta1. La línea 35 pide al usuario que escriba un monto a depositar para cuenta2. La línea 36 obtiene la entrada del usuario, llamando al método nextDouble del objeto Scanner llamado entrada. Las líneas 37 y 38 muestran el monto del depósito. La línea 39 llama al método abonar del objeto cuenta2 y le suministra montoDeposito como argumento; después, el método abonar suma ese valor al saldo. Por último, las líneas 42 a la 45 imprimen en pantalla los saldos de ambos objetos Cuenta otra vez, para mostrar que sólo se modificó el saldo de cuenta2.

Diagrama de clases de UML para la clase Cuenta El diagrama de clases de UML en la figura 3.15 modela la clase Cuenta de la figura 3.13. El diagrama modela la propiedad private llamada saldo con el tipo Double de UML, para que corresponda a la variable de instancia saldo de la clase, que tiene el tipo double de Java. El diagrama modela el constructor de la clase Cuenta con un parámetro saldoInicial del tipo Double de UML en el tercer compartimiento de la clase. Los dos métodos public de la clase se modelan como operaciones en el tercer compartimiento también. El diagrama modela la operación abonar con un parámetro monto de tipo Double de UML (ya que el método correspondiente tiene un parámetro monto de tipo double en Java) y la operación obtenerSaldo con un tipo de valor de retorno Double (ya que el método correspondiente en Java devuelve un valor double).

3.9 (Opcional) Ejemplo práctico de GUI y gráficos: uso de cuadros de diálogo Este ejemplo práctico opcional está diseñado para aquellos quienes desean empezar a conocer las poderosas herramientas de Java para crear interfaces gráficas de usuario (GUIs) y gráficos antes de las principales discusiones de estos temas en el capítulo 11, Componentes de la GUI: parte 1, el capítulo 12, Gráficos y Java 2D™, y el capítulo 22, Componentes de la GUI: parte 2.

www.elsolucionario.net

96

Capítulo 3

Introducción a las clases y los objetos

Cuenta – balance : Double «constructor» Cuenta( saldoInicial : Double ) + abonar( monto : Double ) + obtenerSaldo( ) : Double

Figura 3.15 | Diagrama de clases de UML, el cual indica que la clase Cuenta tiene un atributo private llamado saldo, con el tipo Double de UML, un constructor (con un parámetro de tipo Double de UML) y dos operaciones public: abonar (con un parámetro monto de tipo Double de UML) y obtenerSaldo (devuelve el tipo Double de UML). El ejemplo práctico de GUI y gráficos aparece en 10 secciones breves (figura 3.16). Cada sección introduce unos cuantos conceptos básicos y proporciona ejemplos visuales y gráficos. En las primeras secciones, creará sus primeras aplicaciones gráficas; en las secciones posteriores, utilizará los conceptos de programación orientada a objetos que se presentan a lo largo del capítulo 10 para crear una aplicación de dibujo, la cual dibuja una variedad de figuras. Cuando presentemos formalmente a las GUIs en el capítulo 11, utilizaremos el ratón para elegir exactamente qué figuras dibujar y en dónde dibujarlas. En el capítulo 12, agregaremos las herramientas de la API de gráficos en 2D de Java para dibujar las figuras con distintos grosores de línea y rellenos. Esperamos que este ejemplo práctico le sea informativo y divertido. Ubicación

Título – Ejercicio(s)

Sección 3.9 Sección 4.14 Sección 5.10 Sección 6.13 Sección 7.13 Sección 8.18 Sección 9.8 Sección 10.8 Ejercicio 11.18 Ejercicio 12.31

Uso de cuadros de diálogo: entrada y salida básica con cuadros de diálogo. Creación de dibujos simples: mostrar y dibujar líneas en la pantalla. Dibujo de rectángulos y óvalos: uso de figuras para representar datos. Colores y figuras rellenas: dibujar un tiro al blanco y gráficos aleatorios. Dibujo de arcos: dibujar espirales con arcos. Uso de objetos con gráficos: almacenar figuras como objetos. Mostrar texto e imágenes mediante el uso de etiquetas: proporcionar información de estado. Dibujo con polimorfismo: identificar las similitudes entre figuras. Expansión de la interfaz: uso de componentes de la GUI y manejo de eventos. Agregar Java 2D: uso de la API 2D de Java para mejorar los dibujos.

Figura 3.16 | Glosario de GUI ejemplo práctico en cada capítulo.

Cómo mostrar texto en un cuadro de diálogo Los programas que hemos presentado hasta ahora muestran su salida en la ventana de comandos. Muchas aplicaciones utilizan ventanas, o cuadros de diálogo (también llamados diálogos) para mostrar la salida. Por ejemplo, los navegadores Web como Firefox o Microsoft Internet Explorer muestran las páginas Web en sus propias ventanas. Los programas de correo electrónico le permiten escribir y leer mensajes en una ventana. Por lo general, los cuadros de diálogo son ventanas en las que los programas muestran mensajes importantes a los usuarios. La clase JOptionPane cuenta con cuadros de diálogo previamente empaquetados, los cuales permiten a los programas mostrar ventanas que contengan mensajes; a dichas ventanas se les conoce como diálogos de mensaje. La figura 3.17 muestra el objeto String “Bienvenido\na\nJava” en un diálogo de mensaje. La línea 3 indica que el programa utiliza la clase JOptionPane del paquete javax.swing. Este paquete contiene muchas clases que le ayudan a crear interfaces gráficas de usuario (GUIs) para las aplicaciones. Los componentes de la GUI facilitan la entrada de datos al usuario del programa, y la presentación de los datos de salida. La línea 10 llama al método showMessageDialog de JOptionPane para mostrar un cuadro de diálogo que contiene un mensaje. El método requiere dos argumentos. El primero ayuda a Java a determinar en dónde

www.elsolucionario.net

3.9

1 2 3 4 5 6 7 8 9 10 11 12

Ejemplo práctico de GUI y gráficos: uso de cuadros de diálogo

97

// Fig. 3.17: Dialogo1.java // Imprimir varias líneas en un cuadro de diálogo. import javax.swing.JOptionPane; // importa la clase JOptionPane public class Dialogo1 { public static void main( String args[] ) { // muestra un cuadro de diálogo con un mensaje JOptionPane.showMessageDialog( null, “Bienvenido\na\nJava” ); } // fin de main } // fin de la clase Dialogo1

Figura 3.17 | Uso de JOptionPane para mostrar varias líneas en un cuadro de diálogo. colocar el cuadro de diálogo. Cuando el primer argumento es null, el cuadro de diálogo aparece en el centro de la pantalla de la computadora. El segundo argumento es el objeto String a mostrar en el cuadro de diálogo. El método showMessageDialog es un método static de la clase JOptionPane. A menudo, los métodos static definen las tareas utilizadas con frecuencia, y no se requiere crear explícitamente un objeto. Por ejemplo, muchos programas muestran cuadros de diálogo. En vez de que usted tenga que crear código para realizar esta tarea, los diseñadores de la clase JOptionPane de Java declaran un método static que realiza esta tarea por usted. Por lo general, la llamada a un método static se realiza mediante el uso del nombre de su clase, seguido de un punto (.) y del nombre del método, como en NombreClase.nombreMétodo( argumentos ) El capítulo 6, Métodos: un análisis más detallado, habla sobre los métodos static con detalle.

Introducir texto en un cuadro de diálogo La aplicación de la figura 3.18 utiliza otro cuadro de diálogo JOptionPane predefinido, conocido como diálogo de entrada, el cual permite al usuario introducir datos en el programa. El programa pide el nombre del usuario, y responde con un diálogo de mensaje que contiene un saludo y el nombre introducido por el usuario.

1 2 3 4 5 6 7 8 9 10 11 12 13 14

// Fig. 3.18: DialogoNombre.java // Entrada básica con un cuadro de diálogo. import javax.swing.JOptionPane; public class DialogoNombre { public static void main( String args[] ) { // pide al usuario que escriba su nombre String nombre = JOptionPane.showInputDialog( “Cual es su nombre?” ); // crea el mensaje String mensaje =

Figura 3.18 | Cómo obtener la entrada del usuario mediante un cuadro de diálogo. (Parte 1 de 2).

www.elsolucionario.net

98

15 16 17 18 19 20

Capítulo 3

Introducción a las clases y los objetos

String.format( “Bienvenido, %s, a la programacion en Java!”, nombre ); // muestra el mensaje para dar la bienvenida al usuario por su nombre JOptionPane.showMessageDialog( null, mensaje ); } // fin de main } // fin de la clase DialogoNombre

Figura 3.18 | Cómo obtener la entrada del usuario mediante un cuadro de diálogo. (Parte 2 de 2). Las líneas 10 y 11 utilizan el método showInputDialog de JOptionPane para mostrar un diálogo de entrada que contiene un indicador y un campo (conocido como campo de texto), en el cual el usuario puede escribir texto. El argumento de showInputDialog es el indicador que muestra lo que el usuario debe escribir. 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 devolver el objeto String al programa. El método showInputDialog (línea 11) devuelve un objeto String que contiene los caracteres escritos por el usuario. Almacenamos el objeto String en la variable nombre (línea 10). [Nota: si oprime el botón Cancelar en el cuadro de diálogo, el método devuelve null y el programa muestra la palabra clave “null” como el nombre]. Las líneas 14 y 15 utilizan el método static String llamado format para devolver un objeto String que contiene un saludo con el nombre del usuario. El método format es similar al método System.out.printf, excepto que format devuelve el objeto String con formato, en vez de mostrarlo en una ventana de comandos. La línea 18 muestra el saludo en un cuadro de diálogo de mensaje.

Ejercicio del ejemplo práctico de GUI y gráficos 3.1 Modifique el programa de suma en la figura 2.7 para usar la entrada y salida basadas en cuadro de diálogo con los métodos de la clase JOptionPane. Como el método showInputDialog devuelve un objeto String, debe convertir el objeto String que introduce el usuario a un int para usarlo en los cálculos. El método Integer.parseInt( String s )

toma un argumento String que representa a un entero (por ejemplo, el resultado de JOptionPane.showInputDialog) y devuelve el valor como un int. El método parseInt es un método static de la clase Integer (del paquete java.lang). Observe que si el objeto String no contiene un entero válido, el programa terminará con un error.

3.10 (Opcional) Ejemplo práctico de Ingeniería de Software: identificación de las clases en un documento de requerimientos Ahora empezaremos a diseñar el sistema ATM que presentamos en el capítulo 2. 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 sistema Para 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 implementaremos en Java. Primero debemos revisar el documento de requerimientos de la sección 2.9, 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.

www.elsolucionario.net

3.10

(Opción) Ejemplo práctico de Ingeniería de Software: identificación de las clases en un...

99

La figura 3.19 lista los sustantivos y frases nominales que se encontraron en el documento de requerimientos de la sección 2.9. Los listaremos de izquierda a derecha, en el orden en el que los encontramos por primera vez en el documento de requerimientos. Sólo listaremos la forma singular de cada sustantivo o frase nominal. Sustantivos y frases nominales en el documento de requerimientos del ATM banco ATM usuario cliente transacción cuenta saldo

dinero / fondos pantalla teclado numérico dispensador de efectivo billete de $20 / efectivo ranura de depósito sobre de depósito

número de cuenta NIP base de datos del banco solicitud de saldo retiro depósito

Figura 3.19 | Sustantivos y frases nominales en el documento de requerimientos del ATM. 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 entidades 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 2.20. 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 4.15 asignaremos atributos a las clases del sistema 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 representar la presencia de un sobre en el sistema (en la sección 6.14 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 sentido 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. 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. [Nota: en la sección 10.9, “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 herencia]. Determinaremos las clases para nuestro sistema con base en los sustantivos y frases nominales restantes de la figura 3.19. 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

www.elsolucionario.net

100

Capítulo 3

Introducción a las clases y los objetos

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 convenció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 escribiremos la primera letra de cada una de ellas en mayúscula (por ejemplo, NombreConVariasPalabras). Utilizando esta convención, crearemos las clases ATM, Pantalla, Teclado, DispensadorEfectivo, RanuraDeposito, Cuenta, BaseDatosBanco, SolicitudSaldo, Retiro y Deposito. Construiremos 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 clases UML nos permite modelar, a través de los diagramas de clases, las clases en el sistema ATM y sus interrelaciones. La figura 3.20 representa a la clase ATM. En UML, cada clase se modela como un rectángulo con tres compartimientos. El compartimiento superior contiene el nombre de la clase, centrado horizontalmente y en negrita. El compartimiento intermedio contiene los atributos de la clase (en las secciones 4.15 y 5.11 hablaremos sobre los atributos). El compartimiento inferior contiene las operaciones de la clase (que veremos en la sección 6.14). En la figura 3.20, los compartimientos intermedio e inferior están vacíos, ya que no hemos determinado los atributos y operaciones de esta clase todavía. Los diagramas de clases también muestran las relaciones entre las clases del sistema. La figura 3.21 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 atributos 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 información, como el contenido de los compartimientos segundo y tercero, no se modela. En las secciones 4.15 y 6.14 colocaremos información en estos compartimientos. En la figura 3.21, la línea sólida que conecta a las dos clases representa una asociación: una relación entre clases. Los números cerca de cada extremo de la línea son valores de multiplicidad; éstos indican 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 3.22 lista y explica los tipos de multiplicidad. 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 3.21 indica el nombre de esa asociación. Esta parte del diagrama 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 inapropiado, 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”.

ATM

Figura 3.20 | Representación de una clase en UML mediante un diagrama de clases.

ATM

1

0..1 Ejecuta transaccionActual

Figura 3.21 | Diagrama de clases que muestra una asociación entre clases.

www.elsolucionario.net

Retiro

3.10

(Opción) Ejemplo práctico de Ingeniería de Software: identificación de las clases en un...

Símbolo

Significado

0 1 m 0..1 m, n m..n * 0..* 1..*

Ninguno. Uno. Un valor entero. Cero o uno. m o n. Cuando menos m, pero no más que n. Cualquier entero no negativo (cero o más). Cero o más (idéntico a *). Uno o más.

101

Figura 3.22 | Tipos de multiplicidad. La palabra transaccionActual en el extremo de Retiro de la línea de asociación en la figura 3.21 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 3.21, 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 diagramas 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 complejas, 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 3.23, 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, DispensadorEfectivo 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 3.23 indican que un objeto de la clase ATM está formado por un objeto de la clase Pantalla, un objeto de 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. La relación tiene un define la composición (en la sección del Ejemplo práctico de Ingeniería de Software del capítulo 10 veremos que la relación “es un” define la herencia). De acuerdo con la especificación del UML (www.uml.org), 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 responsable de la creación y destrucción de sus partes. Por ejemplo, el acto de construir un ATM incluye 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.

www.elsolucionario.net

102

Capítulo 3

Introducción a las clases y los objetos

Pantalla 1 1 RanuraDeposito

1

1

ATM

1

1

DispensadorEfectivo

1 1 Teclado

Figura 3.23 | Diagrama de clases que muestra las relaciones de composición.

Los diamantes sólidos en nuestros diagramas de clases indican las relaciones de composición que cumplen con estas tres 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 personal 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 puede conectarse a varias computadoras a la vez, con lo cual se violan las propiedades segunda y tercera de la composición. La figura 3.24 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. [Nota: 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 el capítulo 10 expandiremos nuestro diagrama de clases para incluir todas las clases en el sistema ATM]. La figura 3.24 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 presentamos en las figuras 3.21 o 3.23. 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 en base a un objeto BaseDatosBanco. En la figura 3.24 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. Recuerde que en la figura 3.22 se muestra que 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 BaseDatosBanco; puede haber muchos objetos Cuenta en BaseDatosBanco. [Nota: si recuerda la figura 3.22, el valor de multiplicidad * es idéntico a 0..*. Incluimos 0..* en nuestros diagramas de clases por cuestión de claridad]. La figura 3.24 también indica que si el usuario va a realizar un retiro, “un objeto de la clase Retiro accede a/modifica el saldo de una cuenta a través de un objeto de la clase BaseDatosBanco”. Podríamos haber creado una asociación directamente entre la clase Retiro y la clase Cuenta. No obstante, el documento de requerimientos 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. 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 3.24 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.

www.elsolucionario.net

3.10

(Opción) Ejemplo práctico de Ingeniería de Software: identificación de las clases en un...

103

Aunque no se muestran en la figura 3.24, 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 accede a la ranura de depósitos. Ya hemos identificado las clases 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 4.15 determinaremos los atributos para cada una de estas clases, y en la sección 5.11 utilizaremos estos atributos para examinar la forma en que cambia el sistema con el tiempo.

1 Teclado

1

1

RanuraDeposito

DispensadorEfectivo

1

Pantalla

1

1

1 1

1

1

1

ATM

0..1 Ejecuta 0..1

0..1

0..1

Retiro 0..1

1 Autentica al usuario en base a 1 BaseDatosBanco

Contiene

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

1 0..*

Cuenta

Figura 3.24 | Diagrama de clases para el modelo del sistema ATM.

Ejercicios de autoevaluación del Ejemplo práctico de Ingeniería de Software 3.1

Suponga que tenemos una clase llamada Auto, la cual representa a un automóvil. Piense en algunas de las distintas piezas que podría reunir un fabricante para producir un automóvil completo. Cree un diagrama de clases (similar a la figura 3.23) que modele algunas de las relaciones de composición de la clase Auto. 3.2 Suponga que tenemos una clase llamada Archivo, la cual representa un documento electrónico en una computadora 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.

3.3

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). 3.4 Modifique el diagrama de clases de la figura 3.24 para incluir la clase Deposito, en vez de la clase Retiro.

www.elsolucionario.net

104

Capítulo 3

Introducción a las clases y los objetos

Respuestas a los ejercicios de autoevaluación del Ejemplo práctico de Ingeniería de Software 3.1

[Nota: las respuestas de los estudiantes pueden variar]. La figura 3.25 presenta un diagrama de clases que muestra algunas de las relaciones de composición de una clase Auto. 3.2 c. [Nota: en una computadora con conexión de red, esta relación podría ser de varios a varios]. 3.3 Verdadera. 3.4 La figura 3.26 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 3.24). Observe que la clase Deposito no se asocia con la clase DispensadorEfectivo, sino que se asocia con la clase RanuraDeposito.

Rueda 4 1 Volante

1

1

Auto

1

5

CinturonSeguridad

1 2 Parabrisas

Figura 3.25 | Diagrama de clases que muestra algunas relaciones de composición de una clase Auto.

1 Teclado

1

1

1

RanuraDeposito

DispensadorEfectivo

1

Pantalla

1

1 1

1

1

1

ATM

0..1 Ejecuta 0..1

0..1

0..1

Deposito 0..1

1 Autentica el usuario en base a 1 BaseDatosBanco

Contiene

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

1 0..*

Cuenta

Figura 3.26 | Diagrama de clases para el modelo del sistema ATM, incluyendo la clase Deposito.

www.elsolucionario.net

Resumen

105

3.11 Conclusión En este capítulo aprendió los conceptos básicos de las clases, los objetos, los métodos y las variables de instancia; utilizará estos conceptos en la mayoría de las aplicaciones de Java que vaya a crear. En especial, aprendió a declarar variables de instancia de una clase para mantener los datos de cada objeto de la clase, y cómo declarar métodos que operen sobre esos datos. Aprendió cómo llamar a un método para decirle que realice su tarea y cómo pasar información a los métodos en forma de argumentos. Vio la diferencia entre una variable local de un método y una variable de instancia de una clase, y que sólo las variables de instancia se inicializan en forma automática. También aprendió a utilizar el constructor de una clase para especificar los valores iniciales para las variables de instancia de un objeto. A lo largo del capítulo, vio cómo puede usarse UML para crear diagramas de clases que modelen los constructores, métodos y atributos de las clases. Por último, aprendió acerca de los números de punto flotante: cómo almacenarlos con variables del tipo primitivo double, cómo recibirlos en forma de datos de entrada mediante un objeto Scanner y cómo darles formato con printf y el especificador de formato %f para fines de visualización. En el siguiente capítulo empezaremos nuestra introducción a las instrucciones de control, las cuales especifican el orden en el que se realizan las acciones de un programa. Utilizará estas instrucciones en sus métodos para especificar cómo deben realizar sus tareas.

Resumen Sección 3.2 Clases, objetos, métodos y variables de instancia • Para realizar una tarea en un programa se requiere un método. Dentro del método se colocan los mecanismos que hacen que éste realice sus tareas; es decir, el método oculta los detalles de implementación de las tareas que realiza. • La unidad de programa que aloja a un método se llama clase. Una clase puede contener uno o más métodos, que están diseñados para realizar las tareas de esa clase. • Un método puede realizar una tarea y devolver un resultado. • Puede utilizarse una clase para crear una instancia de la clase, a la cual se le llama objeto. Ésta es una de las razones por las que Java se conoce como lenguaje de programación orientado a objetos. • Cada mensaje que se envía a un objeto se conoce como llamada a un método, y ésta le indica a un método del objeto que realice su tarea. • Cada método puede especificar parámetros que representan la información adicional requerida por el método para realizar su tarea correctamente. La llamada a un método suministra valores (llamados argumentos) para los parámetros del método. • Un objeto tiene atributos que se acarrean con el objeto, a medida que éste se utiliza en un programa. Estos atributos se especifican como parte de la clase del objeto. Los atributos se especifican en las clases mediante campos.

Sección 3.3 Declaración de una clase con un método, e instanciamiento de un objeto de una clase • Cada declaración de clase que empieza con la palabra clave public debe almacenarse en un archivo que tenga exactamente el mismo nombre que la clase, y que termine con la extensión de nombre de archivo .java. • La palabra clave public es un modificador de acceso. • Cada declaración de clase contiene la palabra clave class, seguida inmediatamente por el nombre de la clase. • La declaración de un método que empieza con la palabra clave public indica que el método está “disponible para el público”; es decir, lo pueden llamar otras clases declaradas fuera de la declaración de esa clase. • La palabra clave void indica que un método realizará una tarea, pero no devolverá información cuando la termine. • Por convención, los nombres de los métodos empiezan con la primera letra en minúscula, y todas las palabras subsiguientes en el nombre empiezan con la primera letra en mayúscula. • Los paréntesis vacíos después del nombre de un método indican que éste no requiere parámetros para realizar su tarea. • El cuerpo de todos los métodos está delimitado por llaves izquierda y derecha ({ y }). • El cuerpo de un método contiene instrucciones que realizan la tarea de éste. Una vez que se ejecutan las instrucciones, el método ha terminado su tarea. • Cuando intentamos ejecutar una clase, Java busca el método main de la clase para empezar la ejecución. • Cualquier clase que contenga public static void main( String args[] ) puede usarse para ejecutar una aplicación. • Por lo general, no podemos llamar a un método que pertenece a otra clase, sino hasta crear un objeto de esa clase.

www.elsolucionario.net

106

Capítulo 3

Introducción a las clases y los objetos

• Las expresiones de creación de instancias de clases que empiezan con la palabra clave new crean nuevos objetos. • Para llamar a un método de un objeto, se pone después del nombre de la variable un separador punto (.), el nombre del método y un conjunto de paréntesis, que contienen los argumentos del método. • En UML, cada clase se modela en un diagrama de clases en forma de rectángulo con tres compartimientos. El compartimiento superior contiene el nombre de la clase, centrado horizontalmente y en negrita. El compartimiento intermedio contiene los atributos de la clase, que corresponden a los campos en Java. El compartimiento inferior contiene las operaciones de la clase, que corresponden a los métodos y constructores en Java. • Para modelar las operaciones, UML lista el nombre de la operación, seguido de un conjunto de paréntesis. Un signo más (+) enfrente del nombre de la operación indica que ésta es una operación public en UML (es decir, un método public en Java).

Sección 3.4 Declaración de un método con un parámetro • A menudo, los métodos requieren información adicional para realizar sus tareas. Dicha información adicional se proporciona mediante argumentos en las llamadas a los métodos. • El método nextLine de Scanner lee caracteres hasta encontrar una nueva línea, y después devuelve los caracteres que leyó en forma de un objeto String. • El método next de Scanner lee caracteres hasta encontrar cualquier carácter de espacio en blanco, y después devuelve los caracteres que leyó en forma de un objeto String. • Un método que requiere datos para realizar su tarea debe especificar esto en su declaración, para lo cual coloca información adicional en la lista de parámetros del método. • Cada parámetro debe especificar tanto un tipo como un identificador. • Cuando se hace la llamada a un método, sus argumentos se asignan a sus parámetros. Entonces, el cuerpo del método utiliza las variables de los parámetros para acceder a los valores de los argumentos. • Un método puede especificar varios parámetros, separando un parámetro del otro mediante una coma. • El número de argumentos en la llamada a un método debe coincidir con el número de parámetros en la lista de parámetros de la declaración del método. Además, los tipos de los argumentos en la llamada al método deben ser consistentes con los tipos de los parámetros correspondientes en la declaración del método. • La clase String está en el paquete java.lang que, por lo general se importa de manera implícita en todos los archivos de código fuente. • Hay una relación especial entre las clases que se compilan en el mismo directorio en el disco. De manera predeterminada, se considera que dichas clases están en el mismo paquete, al cual se le conoce como paquete predeterminado. Las clases en el mismo paquete se importan implícitamente en los archivos de código fuente de las otras clases que están en el mismo paquete. Por ende, no se requiere una declaración import cuando una clase en un paquete utiliza a otra clase en el mismo paquete. • No se requiere una declaración import si siempre hacemos referencia a una clase con su nombre de clase completamente calificado. • Para modelar un parámetro de una operación, UML lista el nombre del parámetro, seguido de dos puntos y el tipo del parámetro entre los paréntesis que van después del nombre de la operación. • UML tiene sus propios tipos de datos, similares a los de Java. No todos los tipos de datos de UML tienen los mismos nombres que los tipos correspondientes en Java. • El tipo String de UML corresponde al tipo String de Java.

Sección 3.5 Variables de instancia, métodos establecer y métodos obtener • Las variables que se declaran en el cuerpo de un método específico se conocen como variables locales, y pueden utilizarse sólo en ese método. • Por lo general, una clase consiste en uno o más métodos que manipulan los atributos (datos) pertenecientes a un objeto específico de esa clase. Los atributos se representan como campos en la declaración de una clase. Dichas variables se llaman campos, y se declaran dentro de la declaración de una clase, pero fuera de los cuerpos de las declaraciones de los métodos de esa clase. • Cuando cada objeto de una clase mantiene su propia copia de un atributo, el campo que representa a ese atributo también se conoce como variable de instancia. Cada objeto (instancia) de la clase tiene una instancia separada de la variable en la memoria. • La mayoría de las declaraciones de variables de instancia van precedidas por el modificador de acceso private. Las variables o métodos declarados con el modificador de acceso private sólo están accesibles para los métodos de la clase en la que están declarados. • Al proceso de declarar variables de instancia con el modificador de acceso private se le conoce como ocultamiento de datos.

www.elsolucionario.net

Resumen

107

• Un beneficio de los campos es que todos los métodos de la clase pueden usarlos. Otra diferencia entre un campo y una variable local es que un campo tiene un valor inicial predeterminado, que Java proporciona cuando el programador no especifica el valor inicial del campo, pero una variable local no hace esto. • El valor predeterminado para un campo de tipo String es null. • Cuando se llama a un método que especifica un tipo de valor de retorno y completa su tarea, el método devuelve un resultado al método que lo llamó. • A menudo, las clases proporcionan métodos public para permitir que los clientes de la clase establezcan u obtengan variables de instancia private. Los nombres de estos métodos no necesitan comenzar con establecer u obtener, pero esta convención de nomenclatura es muy recomendada en Java, y requerida para ciertos componentes de software de Java especiales, conocidos como JavaBeans. • UML representa a las variables de instancia como atributos, listando el nombre del atributo, seguido de dos puntos y el tipo del atributo. • En UML, los atributos privados van precedidos por un signo menos (-). • Para indicar el tipo de valor de retorno de una operación, UML coloca dos puntos y el tipo de valor de retorno después de los paréntesis que siguen del nombre de la operación. • Los diagramas de clases de UML no especifican tipos de valores de retorno para las operaciones que no devuelven valores.

Sección 3.6 Comparación entre tipos primitivos y tipos por referencia • En Java, los tipos se dividen en dos categorías: tipos primitivos y tipos por referencia (algunas veces conocidos como tipos no primitivos). Los tipos primitivos son boolean, byte, char, short, int, long, float y double. Todos los demás tipos son por referencia, por lo cual, las clases que especifican los tipos de los objetos, son tipos por referencia. • Una variable de tipo primitivo puede almacenar exactamente un valor de su tipo declarado, en un momento dado. • Las variables de instancia de tipos primitivos se inicializan de manera predeterminada. Las variables de los tipos byte, char, short, int, long, float y double se inicializan con 0. Las variables de tipo boolean se inicializan con false. • Los programas utilizan variables de tipos por referencia (llamadas referencias) para almacenar la ubicación de un objeto en la memoria de la computadora. Dichas variables hacen referencia a los objetos en el programa. El objeto al que se hace referencia puede contener muchas variables de instancia y métodos. • Los campos de tipo por referencia se inicializan de manera predeterminada con el valor null. • Para invocar a los métodos de instancia de un objeto, se requiere una referencia a éste. Una variable de tipo primitivo no hace referencia a un objeto, por lo cual no puede usarse para invocar a un método.

Sección 3.7 Inicialización de objetos con constructores • Un constructor puede usarse para inicializar un objeto de una clase, a la hora de crear este objeto. • Los constructores pueden especificar parámetros, pero no tipos de valores de retorno. • Si no se proporciona un constructor para una clase, el compilador proporciona un constructor predeterminado sin parámetros. • Cuando una clase sólo tiene el constructor predeterminado, sus variables de instancia se inicializan con sus valores predeterminados. Las variables de los tipos char, byte, short, int, long, float y double se inicializan con 0, las variables de tipo boolean se inicializan con false, y las variables de tipo por referencia se inicializan con null. • Al igual que las operaciones, UML modela a los constructores en el tercer compartimiento de un diagrama de clases. Para diferenciar a un constructor en base a las operaciones de una clase, UML coloca la palabra “constructor” entre los signos « y » antes del nombre del constructor.

Sección 3.8 Números de punto flotante y el tipo double • Un número de punto flotante es un número con un punto decimal, como 7.33, 0.0975 o 1000.12345. Java proporciona dos tipos primitivos para almacenar números de punto flotante en la memoria –float y double. La principal diferencia entre estos tipos es que las variables double pueden almacenar números con mayor magnitud y detalle (a esto se le conoce como la precisión del número) que las variables float. • Las variables de tipo float representan números de punto flotante de precisión simple, y tienen siete dígitos significativos. Las variables de tipo double representan números de punto flotante de precisión doble. Éstos requieren el doble de memoria que las variables float y proporcionan 15 dígitos significativos; tienen aproximadamente el doble de precisión de las variables float. • Los valores de punto flotante que aparecen en código fuente se conocen como literales de punto flotante, y son de tipo double de manera predeterminada.

www.elsolucionario.net

108

Capítulo 3

Introducción a las clases y los objetos

• El método nextDouble de Scanner devuelve un valor double. • El especificador de formato %f se utiliza para mostrar valores de tipo float o double. Puede especificarse una precisión entre % y f para representar el número de posiciones decimales que deben mostrarse a la derecha del punto decimal, en el número de punto flotante. • El valor predeterminado para un campo de tipo double es 0.0, y el valor predeterminado para un campo de tipo int es 0.

Terminología %f, especificador de formato « y » (UML) agregación (UML) atributo (UML) campo campo de texto (GUI) clase class, palabra clave cliente de un objeto de una clase compartimiento en un diagrama de clases (UML) componente de interfaz gráfica de usuario (GUI) composición (UML) constructor constructor predeterminado crear un objeto cuadro de diálogo (GUI) cuadro de diálogo de entrada (GUI) cuadro de diálogo de mensaje (GUI) declaración de clase declarar un método diagrama con elementos omitidos (UML) diagrama de clases de UML diálogo (GUI) diamante sólido (UML) double, tipo primitivo encabezado de un método enviar un mensaje establecer, método expresión de creación de instancia de clase float, tipo primitivo instancia de clase instancia de una clase (objeto) instanciar (o crear) un objeto interfaz gráfica de usuario (GUI) invocar a un método JOptionPane, clase (GUI) lenguaje extensible lista de parámetros literal de punto flotante llamada a un método mensaje

método método que hace la llamada modificador de acceso multiplicidad (UML) new, palabra clave next, método de la clase Scanner nextDouble, método de la clase Scanner nextLine, método de la clase Scanner nombre de rol (UML) null, palabra reservada número de punto flotante número de punto flotante de precisión doble número de punto flotante de precisión simple objeto (o instancia) ocultamiento de datos operación (UML) paquete predeterminado parámetro precisión de un número de punto flotante con formato precisión de un valor de punto flotante private, modificador de acceso public, método public, modificador de acceso punto (.), separador referencia referirse a un objeto relación “tiene un” relación de uno a varios (UML) relación de varios a uno (UML) relación de varios a varios (UML) showInputDialog, método de la clase JOptionPane (GUI) showMessageDialog, método de la clase JOptionPane (GUI) tipo de valor de retorno de un método tipo por referencia tipos no primitivos valor inicial predeterminado valor predeterminado variable de instancia variable local void, palabra clave

www.elsolucionario.net

Ejercicios de autoevaluación

109

Ejercicios de autoevaluación 3.1

3.2

3.3 3.4

Complete las siguientes oraciones: a) Una casa es para un plano de construcción lo que un(a) ____________ para una clase. b) Cada declaración de clase que empieza con la palabra clave ____________ debe almacenarse en un archivo que tenga exactamente el mismo nombre de la clase, y que termine con la extensión de nombre de archivo .java. c) Cada declaración de clase contiene la palabra clave ____________, seguida inmediatamente por el nombre de la clase. d) La palabra clave ____________ crea un objeto de la clase especificada a la derecha de la palabra clave. e) Cada parámetro debe especificar un(a) ____________ y un(a) ____________. f ) De manera predeterminada, se considera que las clases que se compilan en el mismo directorio están en el mismo paquete, conocido como ____________. g) Cuando cada objeto de una clase mantiene su propia copia de un atributo, el campo que representa a este atributo se conoce también como ____________. h) Java proporciona dos tipos primitivos para almacenar números de punto flotante en la memoria: _______ _____ y ____________. i) Las variables de tipo double representan a los números de punto flotante ____________. j) El método ____________ de la clase Scanner devuelve un valor double. k) La palabra clave public es un(a) ____________. l) El tipo de valor de retorno ____________ indica que un método realizará una tarea, pero no devolverá información cuando complete su tarea. m) El método ____________ de Scanner lee caracteres hasta encontrar una nueva línea, y después devuelve esos caracteres como un objeto String. n) La clase String está en el paquete ____________. o) No se requiere un(a) ____________ si siempre hacemos referencia a una clase con su nombre de clase completamente calificado. p) Un ____________ es un número con un punto decimal, como 7.33, 0.0975 o 1000.12345. q) Las variables de tipo float representan números de punto flotante ____________. r) El especificador de formato ____________ se utiliza para mostrar valores de tipo float o double. s) Los tipos en Java se dividen en dos categorías: tipos ____________ y tipos ____________. Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué. a) Por convención, los nombres de los métodos empiezan con la primera letra en mayúscula y todas las palabras subsiguientes en el nombre empiezan con la primera letra en mayúscula. b) Una declaración import no es obligatoria cuando una clase en un paquete utiliza a otra clase en el mismo paquete. c) Los paréntesis vacíos que van después del nombre de un método en la declaración de un método indican que éste no requiere parámetros para realizar su tarea. d) Las variables o los métodos declarados con el modificador de acceso private son accesibles sólo para los métodos de la clase en la que se declaran. e) Una variable de tipo primitivo puede usarse para invocar un método. f ) Las variables que se declaran en el cuerpo de un método específico se conocen como variables de instancia, y pueden utilizarse en todos los métodos de la clase. g) El cuerpo de cada método está delimitado por llaves izquierda y derecha ({ y }). h) Las variables locales de tipo primitivo se inicializan de manera predeterminada. i) Las variables de instancia de tipo por referencia se inicializan de manera predeterminada con el valor null. j) Cualquier clase que contenga public static void main( String args[] ) puede usarse para ejecutar una aplicación. k) El número de argumentos en la llamada a un método debe coincidir con el número de parámetros en la lista de parámetros de la declaración del método. l) Los valores de punto flotante que aparecen en código fuente se conocen como literales de punto flotante, y son de tipo float de manera predeterminada. ¿Cuál es la diferencia entre una variable local y un campo? Explique el propósito de un parámetro de un método. ¿Cuál es la diferencia entre un parámetro y un argumento?

www.elsolucionario.net

110

Capítulo 3

Introducción a las clases y los objetos

Respuestas a los ejercicios de autoevaluación 3.1 a) objeto. b) public. c) class. d) new. e) tipo, nombre. f ) paquete predeterminado. g) variable de instancia. h) float, double. i) de precisión doble. j) nextDouble. k) modificador de acceso. l) void. m) nextLine. n) java.lang. o) declaracion import. p) número de punto flotante. q) de precisión simple. r) %f. s) primitivo, por referencia. 3.2 a) Falso. Por convención, los nombres de los métodos empiezan con una primera letra en minúscula y todas las palabras subsiguientes en el nombre empiezan con una letra en mayúscula. b) Verdadero. c) Verdadero. d) Verdadero. e) Falso. Una variable de tipo primitivo no puede usarse para invocar a un método; se requiere una referencia a un objeto para invocar a los métodos de ese objeto. f ) Falso. Dichas variables se llaman variables locales, y sólo se pueden utilizar en el método en el que están declaradas. g) Verdadero. h) Falso. Las variables de instancia de tipo primitivo se inicializan de manera predeterminada. A cada variable local se le debe asignar un valor de manera explícita. i) Verdadero. j) Verdadero. k) Verdadero. l) Falso. Dichas literales son de tipo double de manera predeterminada. 3.3 Una variable local se declara en el cuerpo de un método, y sólo puede utilizarse desde el punto en el que se declaró, hasta el final de la declaración del método. Un campo se declara en una clase, pero no en el cuerpo de alguno de los métodos de la clase. Cada objeto (instancia) de una clase tiene una copia separada de los campos de la clase. Además, los campos están accesibles para todos los métodos de la clase. (En el capítulo 8, Clases y objetos: un análisis más detallado, veremos una excepción a esto). 3.4 Un parámetro representa la información adicional que requiere un método para realizar su tarea. Cada parámetro requerido por un método está especificado en la declaración del método. Un argumento es el valor actual para un parámetro del método. Cuando se llama a un método, los valores de los argumentos se pasan al método, para que éste pueda realizar su tarea.

Ejercicios 3.5

¿Cuál es el propósito de la palabra clave new? Explique lo que ocurre cuando se utiliza en una aplicación.

3.6 ¿Qué es un constructor predeterminado? ¿Cómo se inicializan las variables de instancia de un objeto, si una clase sólo tiene un constructor predeterminado? 3.7

Explique el propósito de una variable de instancia.

3.8 La mayoría de las clases necesitan importarse antes de poder utilizarlas en una aplicación ¿Por qué cualquier aplicación puede utilizar las clases System y String sin tener que importarlas primero? 3.9

Explique cómo utilizaría un programa la clase Scanner, sin importarla del paquete java.util.

3.10 Explique por qué una clase podría proporcionar un método establecer y un método obtener para una variable de instancia. 3.11

Modifique la clase LibroCalificaciones (figura 3.10) de la siguiente manera: a) Incluya una segunda variable de instancia String, que represente el nombre del instructor del curso. b) Proporcione un método establecer para modificar el nombre del instructor, y un método obtener para obtener el nombre. c) Modifique el constructor para especificar dos parámetros: uno para el nombre del curso y otro para el nombre del instructor. d) Modifique el método mostrarMensaje, de tal forma que primero imprima el mensaje de bienvenida y el nombre del curso, y que después imprima "Este curso es presentado por: ", seguido del nombre del instructor.

Use su clase modificada en una aplicación de prueba que demuestre las nuevas capacidades de la clase. 3.12 Modifique la clase Cuenta (figura 3.13) para proporcionar un método llamado cargar, que retire dinero de un objeto Cuenta. Asegure que el monto a cargar no exceda el saldo de Cuenta. Si lo hace, el saldo debe permanecer sin cambio y el método debe imprimir un mensaje que indique "El monto a cargar excede el saldo de la cuenta". Modifique la clase PruebaCuenta (figura 3.14) para probar el método cargar. 3.13 Cree una clase llamada Factura, que una ferretería podría utilizar para representar una factura para un artículo vendido en la tienda. Una Factura debe incluir cuatro piezas de información como variables de instancia: un número de pieza (tipo String), la descripción de la pieza (tipo String), la cantidad de artículos de ese tipo que se van a comprar

www.elsolucionario.net

Ejercicios

111

(tipo int) y el precio por artículo (double). Su clase debe tener un constructor que inicialice las cuatro variables de instancia. Proporcione un método establecer y un método obtener para cada variable de instancia. Además, proporcione un método llamado obtenerMontoFactura, que calcule el monto de la factura (es decir, que multiplique la cantidad por el precio por artículo) y después devuelva ese monto como un valor double. Si la cantidad no es positiva, debe establecerse en 0. Si el precio por artículo no es positivo, debe establecerse a 0.0. Escriba una aplicación de prueba llamada PruebaFactura, que demuestre las capacidades de la clase Factura. 3.14 Cree una clase llamada Empleado, que incluya tres piezas de información como variables de instancia: un primer nombre (tipo String), un apellido paterno (tipo String) y un salario mensual (double). Su clase debe tener un constructor que inicialice las tres variables de instancia. Proporcione un método establecer y un método obtener para cada variable de instancia. Si el salario mensual no es positivo, establézcalo a 0.0. Escriba una aplicación de prueba llamada PruebaEmpleado, que demuestre las capacidades de cada Empleado. Cree dos objetos Empleado y muestre el salario anual de cada objeto. Después, proporcione a cada Empleado un aumento del 10% y muestre el salario anual de cada Empleado otra vez. 3.15 Cree una clase llamada Fecha, que incluya tres piezas de información como variables de instancia —un mes (tipo int), un día (tipo int) y un año (tipo int). Su clase debe tener un constructor que inicialice las tres variables de instancia, y debe asumir que los valores que se proporcionan son correctos. Proporcione un método establecer y un método obtener para cada variable de instancia. Proporcione un método mostrarFecha, que muestre el mes, día y año, separados por barras diagonales (/). Escriba una aplicación de prueba llamada PruebaFecha, que demuestre las capacidades de la clase Fecha.

www.elsolucionario.net

4 Instrucciones de control: parte 1 Desplacémonos un lugar. —Lewis Carroll

La rueda se convirtió en un círculo completo. —William Shakespeare

OBJETIVOS En este capítulo aprenderá a:

¡Cuántas manzanas tuvieron que caer en la cabeza de Newton antes de que entendiera el suceso!

Q

Comprender las técnicas básicas para solucionar problemas.

Q

Desarrollar algoritmos mediante el proceso de refinamiento de arriba a abajo, paso a paso, usando seudocódigo.

Q

Utilizar las estructuras de selección if e if...else para elegir entre distintas acciones alternativas.

Toda la evolución que conocemos procede de lo vago a lo definido.

Q

Utilizar la estructura de repetición while para ejecutar instrucciones de manera repetitiva dentro de un programa.

—Charles Sanders Peirce

Q

Comprender la repetición controlada por un contador y la repetición controlada por un centinela.

Q

Utilizar los operadores de asignación compuestos, de incremento y decremento.

Q

Conocer los tipos de datos primitivos.

www.elsolucionario.net

—Robert Frost

Pla n g e ne r a l

4.2

4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9 4.10 4.11 4.12 4.13 4.14 4.15 4.16

Algoritmos

113

Introducción Algoritmos Seudocódigo Estructuras de control Instrucción de selección simple if Instrucción de selección doble if...else Instrucción de repetición while Cómo formular algoritmos: repetición controlada por un contador Cómo formular algoritmos: repetición controlada por un centinela Cómo formular algoritmos: instrucciones de control anidadas Operadores de asignación compuestos Operadores de incremento y decremento Tipos primitivos (Opcional) Ejemplo práctico de GUI y gráficos: creación de dibujos simples (Opcional) Ejemplo práctico de Ingeniería de Software: identificación de los atributos de las clases Conclusión

Resumen | Terminología | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

4.1 Introducción Antes de escribir un programa que dé solución a un problema, es imprescindible tener una comprensión detallada de todo el problema, además de una metodología cuidadosamente planeada para resolverlo. Al escribir un programa, es igualmente esencial comprender los tipos de bloques de construcción disponibles, y emplear las técnicas comprobadas para construir programas. En este capítulo y en el 5, Instrucciones de control: parte 2, hablaremos sobre estas cuestiones cuando presentemos la teoría y los principios de la programación estructurada. Los conceptos aquí presentados son imprescindibles para crear clases y manipular objetos. En este capítulo presentamos las instrucciones if...else y while de Java, tres de los bloques de construcción que permiten a los programadores especificar la lógica requerida para que los métodos realicen sus tareas. Dedicamos una parte de este capítulo (y de los capítulos 5 y 7) para desarrollar más la clase LibroCalificaciones que presentamos en el capítulo 3. En especial, agregamos un método a la clase LibroCalificaciones que utiliza instrucciones de control para calcular el promedio de un conjunto de calificaciones de estudiantes. Otro ejemplo demuestra formas adicionales de combinar instrucciones de control para resolver un problema similar. Presentamos los operadores de asignación compuestos de Java, y exploramos los operadores de incremento y decremento. Estos operadores adicionales abrevian y simplifican muchas instrucciones de los programas. Por último, presentamos las generalidades acerca de los tipos de datos primitivos que están disponibles para los programadores.

4.2 Algoritmos Cualquier problema de computación puede resolverse ejecutando una serie de acciones en un orden específico. Un procedimiento para resolver un problema en términos de: 1. las acciones a ejecutar y 2. el orden en el que se ejecutan estas acciones se conoce como un algoritmo. El siguiente ejemplo demuestra que es importante especificar de manera correcta el orden en el que se ejecutan las acciones. Considere el “algoritmo para levantarse y arreglarse” que sigue un ejecutivo para levantarse de la cama e ir a trabajar: (1) levantarse; (2) quitarse la pijama; (3) bañarse; (4) vestirse; (5) desayunar; (6) transportarse al trabajo. Esta rutina logra que el ejecutivo llegue al trabajo bien preparado para tomar decisiones críticas. Suponga

www.elsolucionario.net

114

Capítulo 4

Instrucciones de control: parte 1

que los mismos pasos se realizan en un orden ligeramente distinto: (1) levantarse; (2) quitarse la pijama; (3) vestirse; (4) bañarse; (5) desayunar; (6) transportarse al trabajo. En este caso nuestro ejecutivo llegará al trabajo todo mojado. Al proceso de especificar el orden en el que se ejecutan las instrucciones (acciones) en un programa, se le llama control del programa. En este capítulo investigaremos el control de los programas mediante el uso de las instrucciones de control de Java.

4.3 Seudocódigo El seudocódigo es un lenguaje informal que ayuda a los programadores a desarrollar algoritmos sin tener que preocuparse por los estrictos detalles de la sintaxis del lenguaje Java. El seudocódigo que presentaremos es especialmente útil para desarrollar algoritmos que se convertirán en porciones estructuradas de programas en Java. El seudocódigo es similar al lenguaje cotidiano; es conveniente y amigable con el usuario, aunque no es realmente un lenguaje de programación de computadoras. Empezaremos a utilizar el seudocódigo en la sección 4.5, y en la figura 4.5 aparece un programa de seudocódigo de ejemplo. El seudocódigo no se ejecuta en las computadoras. En vez de ello, ayuda al programador a “organizar” un programa antes de que intente escribirlo en un lenguaje de programación como Java. Este capítulo presenta varios ejemplos de cómo utilizar el seudocódigo para desarrollar programas en Java. El estilo de seudocódigo que presentaremos consiste solamente en caracteres, de manera que los programadores pueden escribir el seudocódigo, utilizando cualquier programa editor de texto. Un programa en seudocódigo preparado de manera cuidadosa puede convertirse fácilmente en su correspondiente programa en Java. En muchos casos, esto requiere tan sólo reemplazar las instrucciones en seudocódigo con sus instrucciones equivalentes en Java. Por lo general, el seudocódigo describe sólo las instrucciones que representan las acciones que ocurren después de que un programador convierte un programa de seudocódigo a Java, y el programa se ejecuta en una computadora. Dichas acciones podrían incluir la entrada, salida o un cálculo. Por lo general no incluimos las declaraciones de variables en nuestro seudocódigo, pero algunos programadores optan por listar las variables y mencionar sus propósitos al principio de su seudocódigo.

4.4 Estructuras de control Generalmente, en un programa las instrucciones se ejecutan una después de otra, en el orden en que están escritas. Este proceso se conoce como ejecución secuencial. Varias instrucciones en Java, que pronto veremos, permiten al programador especificar que la siguiente instrucción a ejecutarse tal vez no sea la siguiente en la secuencia. Esto se conoce como transferencia de control. Durante la década de los sesenta, se hizo evidente que el uso indiscriminado de las transferencias de control era el origen de muchas de las dificultades que experimentaban los grupos de desarrollo de software. A quien se señaló como culpable fue a la instrucción goto (utilizada en la mayoría de los lenguajes de programación de esa época), la cual permite al programador especificar la transferencia de control a uno de los muchos posibles destinos dentro de un programa. La noción de la llamada programación estructurada se hizo casi un sinónimo de la “eliminación del goto”. [Nota: Java no tiene una instrucción goto; sin embargo, la palabra goto está reservada para Java y no debe usarse como identificador en los programas]. Las investigaciones de Bohm y Jacopini1 demostraron que los programas podían escribirse sin instrucciones goto. El reto de la época para los programadores fue cambiar sus estilos a una “programación sin goto”. No fue sino hasta la década de los setenta cuando los programadores tomaron en serio la programación estructurada. Los resultados fueron impresionantes. Los grupos de desarrollo de software reportaron reducciones en los tiempos de desarrollo, mayor incidencia de entregas de sistemas a tiempo y más proyectos de software finalizados sin salirse del presupuesto. La clave para estos logros fue que los programas estructurados eran más claros, más fáciles de depurar y modificar, y había más probabilidad de que estuvieran libres de errores desde el principio.

1.

Bohm, C. y G. Jacopini, “Flow Diagrams, Turing Machines and Languages with Only Two Formation Rules”, Communications of the ACM, vol. 9, núm. 5, mayo de 1966, páginas 336-371.

www.elsolucionario.net

4.4

Estructuras de control

115

El trabajo de Bohm y Jacopini demostró que todos los programas podían escribirse en términos de tres estructuras de control solamente: la estructura de secuencia, la estructura de selección y la estructura de repetición. El término “estructuras de control” proviene del campo de las ciencias computacionales. Cuando presentemos las implementaciones de las estructuras de control en Java, nos referiremos a ellas en la terminología de la Especificación del lenguaje Java como “instrucciones de control”.

Estructura de secuencia en Java La estructura de secuencia está integrada en Java. A menos que se le indique lo contrario, la computadora ejecuta las instrucciones en Java una después de otra, en el orden en que estén escritas; es decir, en secuencia. El diagrama de actividad de la figura 4.1 ilustra una estructura de secuencia típica, en la que se realizan dos cálculos en orden. Java permite tantas acciones como deseemos en una estructura de secuencia. Como veremos pronto, en donde quiera que se coloque una sola acción, podrán colocarse varias acciones en secuencia. Los diagramas de actividad son parte de UML. Un diagrama de actividad modela el flujo de trabajo (también conocido como la actividad) de una parte de un sistema de software. Dichos flujos de trabajo pueden incluir una porción de un algoritmo, como la estructura de secuencia de la figura 4.1. Los diagramas de actividad están compuestos por símbolos de propósito especial, como los símbolos de estado de acción (rectángulos cuyos lados izquierdo y derecho se reemplazan con arcos hacia fuera), rombos (diamantes) y pequeños círculos. Estos símbolos se conectan mediante flechas de transición, que representan el flujo de la actividad; es decir, el orden en el que deben ocurrir las acciones. Al igual que el seudocódigo, los diagramas de actividad ayudan a los programadores a desarrollar y representar algoritmos; sin embargo, muchos de ellos aún prefieren el seudocódigo. Los diagramas de actividad muestran claramente cómo operan las estructuras de control. Considere el diagrama de actividad para la estructura de secuencia de la figura 4.1. Este diagrama contiene dos estados de acción que representan las acciones a realizar. Cada estado de acción contiene una expresión de acción (por ejemplo, “sumar calificación a total” o “sumar 1 al contador”), que especifica una acción particular a realizar. Otras acciones podrían incluir cálculos u operaciones de entrada/salida. Las flechas en el diagrama de actividad representan transiciones, las cuales indican el orden en el que ocurren las acciones representadas por los estados de acción. El programa que implementa las actividades ilustradas por el diagrama de la figura 4.1 primero suma calificacion a total, y después suma 1 a contador. El círculo relleno que se encuentra en la parte superior del diagrama de actividad representa el estado inicial de la actividad: el inicio del flujo de trabajo antes de que el programa realice las actividades modeladas. El círculo sólido rodeado por una circunferencia que aparece en la parte inferior del diagrama representa el estado final; es decir, el final del flujo de trabajo después de que el programa realiza sus acciones. La figura 4.1 también incluye rectángulos que tienen la esquina superior derecha doblada. En UML, a estos rectángulos se les llama notas (como los comentarios en Java): comentarios con explicaciones que describen el propósito de los símbolos en el diagrama. La figura 4.1 utiliza las notas de UML para mostrar el código en Java asociado con cada uno de los estados de acción en el diagrama de actividad. Una línea punteada conecta cada nota con el elemento que ésta describe. Los diagramas de actividad generalmente no muestran el código en Java que implementa la actividad. En este libro utilizamos las notas con este propósito, para mostrar cómo se rela-

sumar calificación al total

sumar 1 al contador

Instrucción en Java correspondiente: total = total + calificacion;

Instrucción en Java correspondiente: contador = contador + 1;

Figura 4.1 | Diagrama de actividad de una estructura de secuencia.

www.elsolucionario.net

116

Capítulo 4

Instrucciones de control: parte 1

ciona el diagrama con el código en Java. Para obtener más información sobre UML, vea nuestro ejemplo práctico opcional, que aparece en las secciones tituladas Ejemplo práctico de Ingeniería de Software al final de los capítulos 1 al 8 y 10, o visite www.uml.org.

Instrucciones de selección en Java Java tiene tres tipos de instrucciones de selección (las cuales se describen en este capítulo y en el siguiente). La instrucción if realiza (selecciona) una acción si la condición es verdadera, o evita la acción si la condición es falsa. La instrucción if...else realiza una acción si la condición es verdadera, o realiza una acción distinta si la condición es falsa. La instrucción switch (capítulo 5) realiza una de entre varias acciones distintas, dependiendo del valor de una expresión. La instrucción if es una instrucción de selección simple, ya que selecciona o ignora una sola acción (o, como pronto veremos, un solo grupo de acciones). La instrucción if...else se conoce como instrucción de selección doble, ya que selecciona entre dos acciones distintas (o grupos de acciones). La instrucción switch es una estructura de selección múltiple, ya que selecciona entre diversas acciones (o grupos de acciones).

Instrucciones de repetición en Java Java cuenta con tres instrucciones de repetición (también llamadas instrucciones de ciclo) que permiten a los programas ejecutar instrucciones en forma repetida, siempre y cuando una condición (llamada la condición de continuación del ciclo) siga siendo verdadera. Las instrucciones de repetición se implementan con las instrucciones while, do...while y for. (El capítulo 5 presenta las instrucciones do...while y for). Las instrucciones while y for realizan la acción (o grupo de acciones) en sus cuerpos, cero o más veces; si la condición de continuación del ciclo es inicialmente falsa, no se ejecutará la acción (o grupo de acciones). La instrucción do...while realiza la acción (o grupo de acciones) en su cuerpo, una o más veces. Las palabras if, else, switch, while, do y for son palabras clave en Java; se utilizan para implementar varias características de Java, como las instrucciones de control. Las palabras clave no pueden usarse como identificadores, como los nombres de variables. En el apéndice C aparece una lista completa de las palabras clave en Java.

Resumen de las instrucciones de control en Java Java sólo tiene tres tipos de estructuras de control, a las cuales nos referiremos de aquí en adelante como instrucciones de control: la instrucción de secuencia, las instrucciones de selección (tres tipos) y las instrucciones de repetición (tres tipos). Cada programa se forma combinando tantas instrucciones de secuencia, selección y repetición como sea apropiado para el algoritmo que implemente el programa. Al igual que con la instrucción de secuencia de la figura 4.1, podemos modelar cada una de las instrucciones de control como un diagrama de actividad. Cada diagrama contiene un estado inicial y final, los cuales representan el punto de entrada y salida de la instrucción de control, respectivamente. Las instrucciones de control de una sola entrada/una sola salida facilitan la creación de programas; las instrucciones de control están “unidas” entre sí mediante la conexión del punto de salida de una instrucción de control, al punto de entrada de la siguiente. Este procedimiento es similar a la manera en que un niño apila los bloques de construcción, así que a esto le llamamos apilamiento de instrucciones de control. En breve aprenderemos que sólo hay una manera alternativa de conectar las instrucciones de control: el anidamiento de instrucciones de control, en el cual una instrucción de control aparece dentro de otra. Por lo tanto, los algoritmos en los programas en Java se crean a partir de sólo tres principales tipos de instrucciones de control, que se combinan sólo de dos formas. Ésta es la esencia de la simpleza.

4.5 Instrucción de selección simple if

Los programas utilizan instrucciones de selección para elegir entre los cursos alternativos de acción. Por ejemplo, suponga que la calificación para aprobar un examen es 60. La instrucción en seudocódigo Si la calificación del estudiante es mayor o igual a 60 Imprimir “Aprobado” determina si la condición “la calificación del estudiante es mayor o igual a 60” es verdadera o falsa. Si la condición es verdadera se imprime “Aprobado”, y se “ejecuta” en orden la siguiente instrucción en seudocódigo. (Recuerde que el seudocódigo no es un verdadero lenguaje de programación). Si la condición es falsa se ignora la instrucción

www.elsolucionario.net

4.6

Instrucción de selección doble if...else

117

Imprimir, y se ejecuta en orden la siguiente instrucción en seudocódigo. La sangría de la segunda línea de esta instrucción de selección es opcional, pero se recomienda ya que enfatiza la estructura inherente de los programas estructurados. La instrucción anterior if en seudocódigo puede escribirse en Java de la siguiente manera: if ( calificacionEstudiante >= 60 ) System.out.println( "Aprobado" );

Observe que el código en Java corresponde en gran medida con el seudocódigo. Ésta es una de las propiedades que hace del seudocódigo una herramienta de desarrollo de programas tan útil. La figura 4.2 muestra la instrucción if de selección simple. Esta figura contiene lo que quizá sea el símbolo más importante en un diagrama de actividad: el rombo o símbolo de decisión, el cual indica que se tomará una decisión. El flujo de trabajo continuará a lo largo de una ruta determinada por las condiciones de guardia asociadas de ese símbolo, que pueden ser verdaderas o falsas. Cada flecha de transición que sale de un símbolo de decisión tiene una condición de guardia (especificada entre corchetes, a un lado de la flecha de transición). Si una condición de guardia es verdadera, el flujo de trabajo entra al estado de acción al que apunta la flecha de transición. En la figura 4.2, si la calificación es mayor o igual a 60, el programa imprime “Aprobado” y luego se dirige al estado final de esta actividad. Si la calificación es menor a 60, el programa se dirige inmediatamente al estado final sin mostrar ningún mensaje. La instrucción if es una instrucción de control de una sola entrada/una sola salida. Pronto veremos que los diagramas de actividad para las instrucciones de control restantes también contienen estados iniciales, flechas de transición, estados de acción que indican las acciones a realizar, símbolos de decisión (con sus condiciones de guardia asociadas) que indican las decisiones a tomar, y estados finales. Esto es consistente con el modelo de programación acción/decisión que hemos estado enfatizando. Imagine siete cajones, en donde cada uno contiene sólo un tipo de instrucción de control de Java. Todas las instrucciones de control están vacías. Su tarea es ensamblar un programa a partir de tantas instrucciones de control de cada tipo como lo requiera el algoritmo, combinando esas instrucciones de control en sólo dos formas posibles (apilando o anidando), y después llenando los estados de acción y las decisiones con expresiones de acción y condiciones de guardia, en una manera que sea apropiada para el algoritmo. Hablaremos sobre la variedad de formas en que pueden escribirse las acciones y las decisiones.

[calificacion >= 60]

imprimir “Aprobado”

[calificacion < 60]

Figura 4.2 | Diagrama de actividad en UML de la instrucción if de selección simple.

4.6 Instrucción de selección doble if...else

La instrucción if de selección simple realiza una acción indicada solamente cuando la condición es verdadera (true); de no ser así, se evita dicha acción. La instrucción if...else de selección doble permite al programador especificar una acción a realizar cuando la condición es verdadera, y otra distinta cuando la condición es falsa. Por ejemplo, la instrucción en seudocódigo: Si la calificación del estudiante es mayor o igual a 60 Imprimir “Aprobado” De lo contrario Imprimir “Reprobado”

www.elsolucionario.net

118

Capítulo 4

Instrucciones de control: parte 1

imprime “Aprobado” si la calificación del estudiante es mayor o igual a 60, y, “Reprobado” si la calificación del estudiante es menor a 60. En cualquier caso, después de que ocurre la impresión se “ejecuta”, según la secuencia, la siguiente instrucción en seudocódigo. La instrucción anterior if...else en seudocódigo puede escribirse en Java como if ( calificacion >= 60 ) System.out.println( "Aprobado" ); else System.out.println( "Reprobado" );

Observe que el cuerpo de la instrucción else también tiene sangría. Cualquiera que sea la convención de sangría que usted elija, debe aplicarla consistentemente en todos sus programas. Es difícil leer programas que no obedecen las convenciones de espaciado uniformes.

Buena práctica de programación 4.1 Utilice sangría en ambos cuerpos de instrucciones de una estructura if...else.

Buena práctica de programación 4.2 Si hay varios niveles de sangría, en cada nivel debe aplicarse la misma cantidad de espacio adicional.

La figura 4.3 muestra el flujo de control en la instrucción if...else. Una vez más (además del estado inicial, las flechas de transición y el estado final), los símbolos en el diagrama de actividad de UML representan estados de acción y decisiones. Nosotros seguimos enfatizando este modelo de computación acción/decisión. Imagine de nuevo un cajón profundo que contiene tantas instrucciones if...else vacías como sea necesario para crear cualquier programa en Java. Su trabajo es ensamblar estas instrucciones if...else (apilando o anidando) con cualquier otra estructura de control requerida por el algoritmo. Usted debe llenar los estados de acción y los símbolos de decisión con expresiones de acción y condiciones de guardia que sean apropiadas para el algoritmo que esté desarrollando.

Operador condicional (?:) Java cuenta con el operador condicional (?:), que en ocasiones puede utilizarse en lugar de una instrucción if...else. Éste es el único operador ternario en Java; es decir, que utiliza tres operandos. En conjunto, los operandos y el símbolo ?: forman una expresión condicional. El primer operando (a la izquierda del ?) es una expresión booleana (es decir, una condición que se evalúa a un valor booleano: true o false), el segundo operando (entre el ? y :) es el valor de la expresión condicional si la expresión booleana es verdadera, y el tercer operando (a la derecha de :) es el valor de la expresión condicional si la expresión booleana se evalúa como false. Por ejemplo, la instrucción System.out.println( calificacionEstudiante >= 60 ? "Aprobado" : "Reprobado" );

imprime el valor del argumento de println, que es una expresión condicional. La expresión condicional en esta instrucción produce como resultado la cadena "Aprobado" si la expresión booleana calificacionEstudiante >= 60 es verdadera, o produce como resultado la cadena "Reprobado" si la expresión booleana es falsa. Por lo tanto, esta instrucción con el operador condicional realiza en esencia la misma función que la instrucción if... else que se mostró anteriormente, en esta sección. La precedencia del operador condicional es baja, por lo que toda la expresión condicional se coloca normalmente entre paréntesis. Pronto veremos que las expresiones condicionales pueden usarse en algunas situaciones en las que no se pueden utilizar instrucciones if...else.

Buena práctica de programación 4.3 Las expresiones condicionales son más difíciles de leer que las instrucciones if...else, por lo cual deben usarse para reemplazar sólo a las instrucciones if...else simples que seleccionan uno de dos valores.

Instrucciones if...else anidadas Un programa puede evaluar varios casos colocando instrucciones if...else dentro de otras instrucciones if... else, para crear instrucciones if...else anidadas. Por ejemplo, el siguiente seudocódigo representa una ins-

www.elsolucionario.net

4.6

imprimir “Reprobado”

[calificacion < 60]

Instrucción de selección doble if...else

[calificacion >= 60]

119

Imprimir “Aprobado”

Figura 4.3 | Diagrama de actividad de UML de la instrucción if...else de selección doble. trucción if...else anidada que imprime A para las calificaciones de exámenes mayores o iguales a 90, B para las calificaciones en el rango de 80 a 89, C para las calificaciones en el rango de 70 a 79, D para las calificaciones en el rango de 60 a 69 y F para todas las demás calificaciones: Si la calificación del estudiante es mayor o igual a 90 Imprimir “A” de lo contrario Si la calificación del estudiante es mayor o igual a 80 Imprimir “B” de lo contrario Si la calificación del estudiante es mayor o igual a 70 Imprimir “C” de lo contrario Si la calificación del estudiante es mayor o igual a 60 Imprimir “D” de lo contrario Imprimir “F” Este seudocódigo puede escribirse en Java como if ( calificacionEstudiante >= 90 ) System.out.println( "A" ); else if ( calificacionEstudiante >= 80 ) System.out.println( "B" ); else if ( calificacionEstudiante >= 70 ) System.out.println( "C" ); else if ( calificacionEstudiante >= 60 ) System.out.println( "D" ); else System.out.println( "F" );

Si calificacionEstudiante es mayor o igual a 90, las primeras cuatro condiciones serán verdaderas, pero sólo se ejecutará la instrucción en la parte if de la primera instrucción if...else. Después de que se ejecute esa instrucción, se evita la parte else de la instrucción if...else más “externa”. La mayoría de los programadores en Java prefieren escribir la instrucción if...else anterior así: if ( calificacionEstudiante >= 90 ) System.out.println( "A" ); else if ( calificacionEstudiante >= 80 ) System.out.println( "B" ); else if ( calificacionEstudiante >= 70 )

www.elsolucionario.net

120

Capítulo 4

Instrucciones de control: parte 1

System.out.println( "C" ); else if ( calificacionEstudiante >= 60 ) System.out.println( "D" ); else System.out.println( "F" );

Las dos formas son idénticas, excepto por el espaciado y la sangría, que el compilador ignora. La segunda forma es más popular ya que evita usar mucha sangría hacia la derecha en el código. Dicha sangría a menudo deja poco espacio en una línea de código, forzando a que las líneas se dividan y empeorando la legibilidad del programa.

Problema del else suelto El compilador de Java siempre asocia un else con el if que le precede inmediatamente, a menos que se le indique otra cosa mediante la colocación de llaves ({ y }). Este comportamiento puede ocasionar lo que se conoce como el problema del else suelto. Por ejemplo, if ( x > 5 ) if ( y > 5 ) System.out.println( "x e y son > 5" ); else System.out.println( "x es 5". De lo contrario, parece ser que si x no es mayor que 5, la instrucción else que es parte del if...else produce como resultado la cadena "x es 5" ); else System.out.println( "x es 5"). No obstante, si la segunda condición es falsa se muestra la cadena "x es 5" ); } else System.out.println( "x es califAlta ) califAlta = calificacion; // nueva calificación más alta } // fin de for return califAlta; // devuelve la calificación más alta } // fin del método obtenerMaxima // determina la calificación promedio de la prueba public double obtenerPromedio() { int total = 0; // inicializa el total // suma las calificaciones para un estudiante for ( int calificacion : calificaciones ) total += calificacion; // devuelve el promedio de las calificaciones return (double) total / calificaciones.length; } // fin del método obtenerPromedio // imprime gráfico de barras que muestra la distribución de las calificaciones public void imprimirGraficoBarras() { System.out.println( "Distribucion de calificaciones:" );

Figura 7.14 | La clase LibroCalificaciones que usa un arreglo para almacenar las calificaciones de una prueba. (Parte 2 de 3).

www.elsolucionario.net

282

103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138

Capítulo 7 Arreglos

// almacena la frecuencia de las calificaciones en cada rango de 10 calificaciones int frecuencia[] = new int[ 11 ]; // para cada calificación, incrementa la frecuencia apropiada for ( int calificacion : calificaciones ) ++frecuencia[ calificacion / 10 ]; // para cada frecuencia de calificación, imprime una barra en el gráfico for ( int cuenta = 0; cuenta < frecuencia.length; cuenta++ ) { // imprime etiquetas de las barras ( "00-09: ", ..., "90-99: ", "100: " ) if ( cuenta == 10 ) System.out.printf( "%5d: ", 100 ); else System.out.printf( "%02d-%02d: ", cuenta * 10, cuenta * 10 + 9 ); // imprime barra de asteriscos for ( int estrellas = 0; estrellas < frecuencia[ cuenta ]; estrellas++ ) System.out.print( "*" ); System.out.println(); // inicia una nueva línea de salida } // fin de for externo } // fin del método imprimirGraficoBarras // imprime el contenido del arreglo de calificaciones public void imprimirCalificaciones() { System.out.println( "Las calificaciones son:\n" ); // imprime la calificación de cada estudiante for ( int estudiante = 0; estudiante < calificaciones.length; estudiante++ ) System.out.printf( "Estudiante %2d: %3d\n", estudiante + 1, calificaciones[ estudiante ] ); } // fin del método imprimirCalificaciones } // fin de la clase LibroCalificaciones

Figura 7.14 | La clase LibroCalificaciones que usa un arreglo para almacenar las calificaciones de una prueba. (Parte 3 de 3).

El método procesarCalificaciones (líneas 37 a 51) contiene una serie de llamadas a métodos que produce un reporte en el que se resumen las calificaciones. La línea 40 llama al método imprimirCalificaciones para imprimir el contenido del arreglo calificaciones. Las líneas 134 a 136 en el método imprimirCalificaciones utilizan una instrucción for para imprimir las calificaciones de los estudiantes. En este caso se debe utilizar una instrucción for controlada por contador, ya que las líneas 135 y 136 utilizan el valor de la variable contador estudiante para imprimir cada calificación enseguida de un número de estudiante específico (vea la figura 7.15). Aunque los subíndices de los arreglos empiezan en 0, lo común es que el profesor enumere a los estudiantes empezando desde 1. Por ende, las líneas 135 y 136 imprimen estudiante + 1 como el número de estudiante para producir las etiquetas "Estudiante 1: ", "Estudiante 2: ", y así en lo sucesivo. A continuación, el método procesarCalificaciones llama al método obtenerPromedio (línea 43) para obtener el promedio de las calificaciones en el arreglo. El método obtenerPromedio (líneas 86 a 96) utiliza una instrucción for mejorada para totalizar los valores en el arreglo calificaciones antes de calcular el promedio. El parámetro en el encabezado de la instrucción for mejorada (por ejemplo, int calificacion) indica que para cada iteración, la variable int calificacion recibe un valor en el arreglo calificaciones. Observe que el cálculo del promedio en la línea 95 utiliza calificaciones.length para determinar el número de calificaciones que se van a promediar.

www.elsolucionario.net

7.8

Ejemplo práctico: la clase LibroCalificaciones que usa un arreglo para almacenar las calificaciones 283

Las líneas 46 y 47 en el método procesarCalificaciones llaman a los métodos obtenerMinima y obtepara determinar las calificaciones más baja y más alta de cualquier estudiante en el examen, en forma respectiva. Cada uno de estos métodos utiliza una instrucción for mejorada para iterar a través del arreglo calificaciones. Las líneas 59 a 64 en el método obtenerMinima iteran a través del arreglo. Las líneas 62 y 63 comparan cada calificación con califBaja; si una calificación es menor que califBaja, a califBaja se le asigna esa calificación. Cuando la línea 66 se ejecuta, califBaja contiene la calificación más baja en el arreglo. El método obtenerMaxima (líneas 70 a 83) funciona de manera similar al método obtenerMinima. Por último, la línea 50 en el método procesarCalificaciones llama al método imprimirGraficoBarras para imprimir un gráfico de distribución de los datos de las calificaciones, mediante el uso de una técnica similar a la de la figura 7.6. En ese ejemplo, calculamos en forma manual el número de calificaciones en cada categoría (es decir, de 0 a 9, de 10 a 19, …, de 90 a 99 y 100) con sólo analizar un conjunto de calificaciones. En este ejemplo, las líneas 107 y 108 utilizan una técnica similar a la de las figuras 7.7 y 7.8 para calcular la frecuencia de las calificaciones en cada categoría. La línea 104 declara y crea el arreglo frecuencia de 11 valores int para almacenar la frecuencia de las calificaciones en cada categoría de éstas. Para cada calificacion en el arreglo calificaciones, las líneas 107 y 108 incrementan el elemento apropiado del arreglo frecuencia. Para determinar qué elemento se debe incrementar, la línea 108 divide la calificacion actual entre 10, mediante la división entera. Por ejemplo, si calificacion es 85, la línea 108 incrementa frecuencia[ 8 ] para actualizar la cuenta de calificaciones en el rango 80-89. Las líneas 111 a 125 imprimen a continuación el gráfico de barras (vea la figura 7.15), con base en los valores en el arreglo frecuencia. Al igual que las líneas 23 y 24 de la figura 7.6, las líneas 121 y 122 de la figura 7.14 utilizan un valor en el arreglo frecuencia para determinar el número de asteriscos a imprimir en cada barra. nerMaxima

La clase PruebaLibroCalificaciones para demostrar la clase LibroCalificaciones La aplicación de la figura 7.15 crea un objeto de la clase LibroCalificaciones (figura 7.14) mediante el uso del arreglo int arregloCalif (que se declara y se inicializa en la línea 10). Las líneas 12 y 13 pasan el nombre de un curso y arregloCalif al constructor de LibroCalificaciones. La línea 14 imprime un mensaje de bienvenida, y la línea 15 invoca el método procesarCalificaciones del objeto LibroCalificaciones. La salida muestra el resumen de las 10 calificaciones en miLibroCalificaciones.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

// Fig. 7.15: PruebaLibroCalificaciones.java // Crea objeto LibroCalificaciones, usando un arreglo de calificaciones. public class PruebaLibroCalificaciones { // el método main comienza la ejecución del programa public static void main( String args[] ) { // arreglo unidimensional de calificaciones de estudiantes int arregloCalif[] = { 87, 68, 94, 100, 83, 78, 85, 91, 76, 87 }; LibroCalificaciones miLibroCalificaciones = new LibroCalificaciones( “CS101 Introduccion a la programacion en Java”, arregloCalif ); miLibroCalificaciones.mostrarMensaje(); miLibroCalificaciones.procesarCalificaciones(); } // fin de main } // fin de la clase PruebaLibroCalificaciones

Bienvenido al libro de calificaciones para CS101 Introduccion a la programacion en Java!

Figura 7.15 | PruebaLibroCalificaciones crea un objeto LibroCalificaciones usando un arreglo de calificaciones, y después invoca al método procesarCalificaciones para analizarlas. (Parte 1 de 2).

www.elsolucionario.net

284

Capítulo 7 Arreglos

Las calificaciones son: Estudiante 1: 87 Estudiante 2: 68 Estudiante 3: 94 Estudiante 4: 100 Estudiante 5: 83 Estudiante 6: 78 Estudiante 7: 85 Estudiante 8: 91 Estudiante 9: 76 Estudiante 10: 87 El promedio de la clase es 84.90 La calificacion mas baja es 68 La calificacion mas alta es 100 Distribucion de calificaciones: 00-09: 10-19: 20-29: 30-39: 40-49: 50-59: 60-69: * 70-79: ** 80-89: **** 90-99: ** 100: *

Figura 7.15 | PruebaLibroCalificaciones crea un objeto LibroCalificaciones usando un arreglo de calificaciones, y después invoca al método procesarCalificaciones para analizarlas. (Parte 2 de 2).

Observación de ingeniería de software 7.1 Un arnés de prueba (o aplicación de prueba) es responsable de crear un objeto de la clase que se probará y de proporcionarle datos. Estos datos podrían provenir de cualquiera de varias fuentes. Los datos de prueba pueden colocarse directamente en un arreglo con un inicializador de arreglos, pueden provenir del usuario mediante el teclado, de un archivo (como veremos en el capítulo 14) o pueden provenir de una red (como veremos en el capítulo 24). Después de pasar estos datos al constructor de la clase para instanciar el objeto, este arnés de prueba debe llamar al objeto para probar sus métodos y manipular sus datos. La recopilación de datos en el arnés de prueba de esta forma permite a la clase manipular datos de varias fuentes.

7.9 Arreglos multidimensionales Los arreglos multidimensionales de dos dimensiones se utilizan con frecuencia para representar tablas de valores, las cuales consisten en información ordenada en filas y columnas. Para identificar un elemento específico de una tabla, debemos especificar dos subíndices. Por convención, el primero identifica la fila del elemento y el segundo su columna. Los arreglos que requieren dos subíndices para identificar un elemento específico se llaman arreglos bidimensionales (los arreglos multidimensionales pueden tener más de dos dimensiones). Java no soporta los arreglos multidimensionales directamente, pero permite al programador especificar arreglos unidimensionales, cuyos elementos sean también arreglos unidimensionales, con lo cual se obtiene el mismo efecto. La figura 7.16 ilustra un arreglo bidimensional a, que contiene tres filas y cuatro columnas (es decir, un arreglo de tres por cuatro). En general, a un arreglo con m filas y n columnas se le llama arreglo de m por n. Cada elemento en el arreglo a se identifica en la figura 7.16 mediante una expresión de acceso a un arreglo de la forma a [ fila ][ columna ]; a es el nombre del arreglo, fila y columna son los subíndices que identifican en forma única a cada elemento en el arreglo a por número de fila y columna. Observe que los nombres de los

www.elsolucionario.net

7.9 Arreglos multidimensionales

Columna 0

Columna 1

Columna 2

Columna 3

Fila 0

a[ 0 ][ 0 ]

a[ 0 ][ 1 ]

a[ 0 ][ 2 ]

a[ 0 ][ 3 ]

Fila 1

a[ 1 ][ 0 ]

a[ 1 ][ 1 ]

a[ 1 ][ 2 ]

a[ 1 ][ 3 ]

Fila 2

a[ 2 ][ 0 ]

a[ 2 ][ 1 ]

a[ 2 ][ 2 ]

a[ 2 ][ 3 ]

285

Subíndice de columna Subíndice de fila Nombre del arreglo

Figura 7.16 | Arreglo bidimensional con tres filas y cuatro columnas. elementos en la fila 0 tienen todos un primer subíndice de 0, y los nombres de los elementos en la columna 3 tienen un segundo subíndice de 3.

Arreglos de arreglos unidimensionales Al igual que los arreglos unidimensionales, los arreglos multidimensionales pueden inicializarse mediante inicializadores de arreglos en las declaraciones. Un arreglo bidimensional b con dos filas y dos columnas podría declararse e inicializarse con inicializadores de arreglos anidados, como se muestra a continuación: int b[ ] [ ] = { { 1, 2 }, {3, 4} };

Los valores del inicializador se agrupan por fila entre llaves. Así, 1 y 2 inicializan a b[ 0 ][ 0 ] y b[ 0 ][ 1 ], respectivamente; 3 y 4 inicializan a b[ 1 ][ 0 ] y b[ 1 ][ 1 ], respectivamente. El compilador cuenta el número de inicializadores de arreglos anidados (representados por conjuntos de llaves dentro de las llaves externas) en la declaración del arreglo, para determinar el número de filas en el arreglo b. El compilador cuenta los valores inicializadores en el inicializador de arreglos anidado de una fila, para determinar el número de columnas en esa fila. Como veremos en unos momentos, esto significa que las filas pueden tener distintas longitudes. Los arreglos multidimensionales se mantienen como arreglos de arreglos unidimensionales. Por lo tanto, el arreglo b en la declaración anterior está realmente compuesto de dos arreglos unidimensionales separados: uno que contiene los valores en la primera lista inicializadora anidada { 1, 2 } y uno que contiene los valores en la segunda lista inicializadora anidada { 3, 4 }. Así, el arreglo b en sí es un arreglo de dos elementos, cada uno de los cuales es un arreglo unidimensional de valores int.

Arreglos bidimensionales con filas de distintas longitudes La forma en que se representan los arreglos multidimensionales los hace bastante flexibles. De hecho, las longitudes de las filas en el arreglo b no tienen que ser iguales. Por ejemplo, int b[ ][ ] = { { 1, 2 }, { 3, 4, 5 } };

crea el arreglo entero b con dos elementos (los cuales se determinan según el número de inicializadores de arreglos anidados) que representan las filas del arreglo bidimensional. Cada elemento de b es una referencia a un arreglo unidimensional de variables int. El arreglo int de la fila 0 es un arreglo unidimensional con dos elementos (1 y 2), y el arreglo int de la fila 1 es un arreglo unidimensional con tres elementos (3, 4 y 5).

Creación de arreglos bidimensionales mediante expresiones de creación de arreglos Un arreglo multidimensional con el mismo número de columnas en cada fila puede crearse mediante una expresión de creación de arreglos. Por ejemplo, en las siguientes líneas se declara el arreglo b y se le asigna una referencia a un arreglo de tres por cuatro: int b[ ][ ] = new int[ 3 ][ 4 ];

www.elsolucionario.net

286

Capítulo 7 Arreglos

En este caso, utilizamos los valores literales 3 y 4 para especificar el número de filas y columnas, respectivamente, pero esto no es obligatorio. Los programas también pueden utilizar variables para especificar las dimensiones de los arreglos, ya que new crea arreglos en tiempo de ejecución, no en tiempo de compilación. Al igual que con los arreglos unidimensionales, los elementos de un arreglo multidimensional se inicializan cuando se crea el objeto arreglo. Un arreglo multidimensional, en el que cada fila tiene un número distinto de columnas, puede crearse de la siguiente manera: int b[][] = new int[ 2 ][ ]; // crea 2 filas b[ 0 ] = new int[ 5 ]; // crea 5 columnas para la fila 0 b[ 1 ] = new int[ 3 ]; // crea 3 columnas para la fila 1

Estas instrucciones crean un arreglo bidimensional con dos filas. La fila 0 tiene cinco columnas y la fila 1 tiene 3.

Ejemplo de arreglos bidimensionales: cómo mostrar los valores de los elementos La figura 7.17 demuestra cómo inicializar arreglos bidimensionales con inicializadores de arreglos, y cómo utilizar ciclos for anidados para recorrer los arreglos (es decir, manipular cada uno de los elementos de cada arreglo). El método main de la clase InicArreglo declara dos arreglos. En la declaración de arreglo1 (línea 9) se utilizan inicializadores de arreglos anidados para inicializar la primera fila del arreglo con los valores 1, 2 y 3, y la segunda fila con los valores 4, 5 y 6. En la declaración de arreglo2 (línea 10) se utilizan inicializadores anidados de distintas longitudes. En este caso, la primera fila se inicializa para tener dos elementos con los valores 1 y 2, respectivamente. La segunda fila se inicializa para tener un elemento con el valor 3. La tercera fila se inicializa para tener tres elementos con los valores 4, 5 y 6, respectivamente.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

// Fig. 7.17: InicArreglo.java // Inicialización de arreglos bidimensionales. public class InicArreglo { // crea e imprime arreglos bidimensionales public static void main( String args[] ) { int arreglo1[][] = { { 1, 2, 3 }, { 4, 5, 6 } }; int arreglo2[][] = { { 1, 2 }, { 3 }, { 4, 5, 6 } }; System.out.println( "Los valores en arreglo1 por filas son" ); imprimirArreglo( arreglo1 ); // muestra arreglo1 por filas System.out.println( "\nLos valores en arreglo2 por filas son" ); imprimirArreglo( arreglo2 ); // muestra arreglo2 por filas } // fin de main // imprime filas y columnas de un arreglo bidimensional public static void imprimirArreglo( int arreglo[][] ) { // itera a través de las filas del arreglo for ( int fila = 0; fila < arreglo.length; fila++ ) { // itera a través de las columnas de la fila actual for ( int columna = 0; columna < arreglo[ fila ].length; columna++ ) System.out.printf( "%d ", arreglo[ fila ][ columna ] ); System.out.println(); // inicia nueva línea de salida } // fin de for externo } // fin del método imprimirArreglo } // fin de la clase InicArreglo

Figura 7.17 | Inicialización de arreglos bidimensionales. (Parte 1 de 2).

www.elsolucionario.net

7.9 Arreglos multidimensionales

287

Los valores en arreglo1 por filas son 1 2 3 4 5 6 Los valores en arreglo2 por filas son 1 2 3 4 5 6

Figura 7.17 | Inicialización de arreglos bidimensionales. (Parte 2 de 2).

Las líneas 13 y 16 llaman al método imprimirArreglo (líneas 20 a 31) para imprimir los elementos de arreglo1 y arreglo2, respectivamente. El método imprimirArreglo especifica el parámetro tipo arreglo como int arreglo[][] para indicar que el método recibe un arreglo bidimensional. La instrucción for (líneas 23 a 30) imprime las filas de un arreglo bidimensional. En la condición de continuación de ciclo de la instrucción exterior, la expresión arreglo.length determina el número de filas en el arreglo. En la expresión for interior, la expresión arreglo[ fila ].length determina el número de columnas en la fila actual del arreglo. Esta condición permite al ciclo determinar el número exacto de columnas en cada fila.

for

Manipulaciones comunes en arreglos multidimensionales, realizadas mediante instrucciones for En muchas manipulaciones comunes en arreglos se utilizan instrucciones for. Como ejemplo, la siguiente instrucción for asigna a todos los elementos en la fila 2 del arreglo a, en la figura 7.16, el valor de cero: for ( int columna = 0; columna < a[ 2 ].length; columna++ ) a[ 2 ][ columna ] = 0;

Especificamos la fila 2; por lo tanto, sabemos que el primer índice siempre será 2 (0 es la primera fila y 1 es la segunda). Este ciclo for varía solamente el segundo índice (es decir, el índice de la columna). Si la fila 2 del arreglo a contiene cuatro elementos, entonces la instrucción for anterior es equivalente a las siguientes instrucciones de asignación: a[ a[ a[ a[

2 2 2 2

][ ][ ][ ][

0 1 2 3

] ] ] ]

= = = =

0; 0; 0; 0;

La siguiente instrucción for anidada suma el total de los valores de todos los elementos del arreglo a: int total = 0; for ( int fila = 0; fila < a.length; fila++ ) { for ( int columna = 0; columna < a[ fila ].length; columna++ ) total += a[ fila ][ columna ]; } // fin de for exterior

Estas instrucciones for anidadas suman el total de los elementos del arreglo, una fila a la vez. La instrucción for exterior empieza asignando 0 al índice fila, de manera que los elementos de la primera fila puedan totalizarse mediante la instrucción for interior. Después, la instrucción for exterior incrementa fila a 1, de manera que la segunda fila pueda totalizarse. Luego, la instrucción for exterior incrementa fila a 2, para que la tercera fila pueda totalizarse. La variable total puede mostrarse al terminar la instrucción for exterior. En el siguiente ejemplo le mostraremos cómo procesar un arreglo bidimensional de una manera similar, usando instrucciones for mejoradas anidadas.

www.elsolucionario.net

288

Capítulo 7 Arreglos

7.10 Ejemplo práctico: la clase LibroCalificaciones que usa un arreglo bidimensional En la sección 7.8 presentamos la clase LibroCalificaciones (figura 7.14), la cual utilizó un arreglo unidimensional para almacenar las calificaciones de los estudiantes en un solo examen. En la mayoría de los cursos, los estudiantes presentan varios exámenes. Es probable que los profesores quieran analizar las calificaciones a lo largo de todo el curso, tanto para un solo estudiante como para la clase en general.

Cómo almacenar las calificaciones de los estudiantes en un arreglo bidimensional en la clase LibroCalificaciones

La figura 7.18 contiene una versión de la clase LibroCalificaciones que utiliza un arreglo bidimensional llamado calificaciones, para almacenar las calificaciones de un número de estudiantes en varios exámenes. Cada fila del arreglo representa las calificaciones de un solo estudiante durante todo el curso, y cada columna representa las calificaciones para la clase completa en uno de los exámenes que presentaron los estudiantes durante el curso. Una aplicación como PruebaLibroCalificaciones (figura 7.19) pasa el arreglo como argumento para el constructor de LibroCalificaciones. En este ejemplo, utilizamos un arreglo de diez por tres que contiene diez calificaciones de los estudiantes en tres exámenes. Cinco métodos realizan manipulaciones de arreglos para procesar las calificaciones. Cada método es similar a su contraparte en la versión anterior de la clase LibroCalificaciones con un arreglo unidimensional (figura 7.14). El método obtenerMinima (líneas 52 a 70) determina la calificación más baja de cualquier estudiante durante el semestre. El método obtenerMaxima (líneas 73 a 91) determina la calificación más alta de cualquier estudiante durante el semestre. El método obtenerPromedio (líneas 94 a 104) determina el promedio semestral de un estudiante específico. El método imprimirGraficoBarras (líneas 107 a 137) imprime un gráfico de barras de la distribución de todas las calificaciones de los estudiantes durante el semestre. El método imprimirCalificaciones (líneas 140 a 164) imprime el arreglo bidimensional en formato tabular, junto con el promedio semestral de cada estudiante. Cada uno de los métodos obtenerMinima, obtenerMaxima e imprimirGraficoBarras itera a través del arreglo calificaciones mediante el uso de instrucciones for anidadas; por ejemplo, la instrucción for mejorada anidada de la declaración del método obtenerMinima (líneas 58 a 67). La instrucción for mejorada exterior itera a través del arreglo bidimensional calificaciones, asignando filas sucesivas al parámetro califEstudiantes en las iteraciones sucesivas. Los corchetes que van después del nombre del parámetro indican que califEstudiantes se refiere a un arreglo int unidimensional: a saber, una fila en el arreglo calificaciones, que contiene las calificaciones de un estudiante. Para buscar la calificación más baja en general, la instrucción for interior compara los elementos del arreglo unidimensional actual califEstudiantes a la variable califBaja. Por ejemplo, en la primera iteración del for exterior, la fila 0 de calificaciones se asigna al parámetro califEstudiantes. Después, la instrucción for mejorada interior itera a través de califEstudiantes y compara cada valor de calificacion con califBaja. Si una calificación es menor que califBaja, a califBaja se le asigna esa calificación. En la segunda iteración de la instrucción for mejorada exterior, la fila 1 de calificaciones se asigna a califEstudiantes, y los elementos de esta fila se comparan con la variable califBaja. Esto se repite hasta que se hayan recorrido todas las filas de calificaciones. Cuando se completa la ejecución de la instrucción anidada, califBaja contiene la calificación más baja en el arreglo bidimensional. El método obtenerMaxima trabaja en forma similar al método obtenerMinima. El método imprimirGraficoBarras en la figura 7.18 es casi idéntico al de la figura 7.14. Sin embargo, para imprimir la distribución de calificaciones en general durante todo un semestre, el método aquí utiliza una instrucción for mejorada anidada (líneas 115 a 119) para crear el arreglo unidimensional frecuencia, con base en todas las calificaciones en el arreglo bidimensional. El resto del código en cada uno de los dos métodos imprimirGraficoBarras que muestran el gráfico es idéntico. El método imprimirCalificaciones (líneas 140 a 164) utiliza instrucciones for anidadas para imprimir valores del arreglo calificaciones, además del promedio semestral de cada estudiante. La salida en la figura 7.19 muestra el resultado, el cual se asemeja al formato tabular del libro de calificaciones real de un profesor. Las líneas 146 y 147 imprimen los encabezados de columna para cada prueba. Aquí utilizamos una instrucción for controlada por contador, para poder identificar cada prueba con un número. De manera similar, la instrucción for en las líneas 152 a 163 imprime primero una etiqueta de fila mediante el uso de una variable contador para identificar a cada estudiante (línea 154). Aunque los subíndices de los arreglos empiezan en 0, observe que las líneas 147 y 154 imprimen prueba + 1 y estudiante + 1 en forma respectiva, para producir números de

www.elsolucionario.net

7.10

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54

Ejemplo práctico: la clase LibroCalificaciones que usa un arreglo bidimensional

289

// Fig. 7.18: LibroCalificaciones.java // Libro de calificaciones que utiliza un arreglo bidimensional para almacenar calificaciones. public class LibroCalificaciones { private String nombreDelCurso; // nombre del curso que representa este LibroCalificaciones private int calificaciones[][]; // arreglo bidimensional de calificaciones de estudiantes // el constructor con dos argumentos inicializa nombreDelCurso y el arreglo calificaciones public LibroCalificaciones( String nombre, int arregloCalif[][] ) { nombreDelCurso = nombre; // inicializa nombreDelCurso calificaciones = arregloCalif; // almacena calificaciones } // fin del constructor de LibroCalificaciones con dos argumentos // método para establecer el nombre del curso public void establecerNombreDelCurso( String nombre ) { nombreDelCurso = nombre; // almacena el nombre del curso } // fin del método establecerNombreDelCurso // método para obtener el nombre del curso public String obtenerNombreDelCurso() { return nombreDelCurso; } // fin del método obtenerNombreDelCurso // muestra un mensaje de bienvenida al usuario de LibroCalificaciones public void mostrarMensaje() { // obtenerNombreDelCurso obtiene el nombre del curso System.out.printf( "Bienvenido al libro de calificaciones para\n%s!\n\n", obtenerNombreDelCurso() ); } // fin del método mostrarMensaje // realiza varias operaciones sobre los datos public void procesarCalificaciones() { // imprime el arreglo de calificaciones imprimirCalificaciones(); // llama a los métodos obtenerMinima y obtenerMaxima System.out.printf( "\n%s %d\n%s %d\n\n ", "La calificación mas baja en el libro de calificaciones es", obtenerMinima(), "La calificación mas alta en el libro de calificaciones es", obtenerMinima() ); // imprime gráfico de distribución de calificaciones para todas las pruebas imprimirGraficoBarras(); } // fin del método procesarCalificaciones // busca la calificación más baja public int obtenerMinima() { // asume que el primer elemento del arreglo calificaciones es el más bajo

Figura 7.18 | Clase LibroCalificaciones que utiliza un arreglo bidimensional para almacenar calificaciones. (Parte 1 de 3).

www.elsolucionario.net

290

55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111

Capítulo 7 Arreglos

int califBaja = calificaciones[ 0 ][ 0 ]; // itera a través de las filas del arreglo calificaciones for ( int califEstudiantes[] : calificaciones ) { // itera a través de las columnas de la fila actual for ( int calificacion : califEstudiantes ) { // si la calificación es menor que califBaja, la asigna a califBaja if ( calificacion < califBaja ) califBaja = calificacion; } // fin de for interior } // fin de for exterior return califBaja; // devuelve calificación más baja } // fin del método obtenerMinima // busca la calificación más alta public int obtenerMaxima() { // asume que el primer elemento del arreglo calificaciones es el más alto int califAlta = calificaciones[ 0 ][ 0 ]; // itera a través de las filas del arreglo calificaciones for ( int califEstudiantes[] : calificaciones ) { // itera a través de las columnas de la fila actual for ( int calificacion : califEstudiantes ) { // si la calificación es mayor que califAlta, la asigna a califAlta if ( calificacion > califAlta ) califAlta = calificacion; } // fin de for interior } // fin de for exterior return califAlta; // devuelve la calificación más alta } // fin del método obtenerMaxima // determina la calificación promedio para un estudiante específico (o conjunto de calificaciones) public double obtenerPromedio( int conjuntoDeCalif[] ) { int total = 0; // inicializa el total // suma las calificaciones para un estudiante for ( int calificacion : conjuntoDeCalif ) total += calificacion; // devuelve el promedio de calificaciones return (double) total / conjuntoDeCalif.length; } // fin del método obtenerPromedio // imprime gráfico de barras que muestra la distribución de calificaciones en general public void imprimirGraficoBarras() { System.out.println( "Distribucion de calificaciones en general:" ); // almacena la frecuencia de las calificaciones en cada rango de 10 calificaciones

Figura 7.18 | Clase LibroCalificaciones que utiliza un arreglo bidimensional para almacenar calificaciones. (Parte 2 de 3).

www.elsolucionario.net

7.10

112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165

Ejemplo práctico: la clase LibroCalificaciones que usa un arreglo bidimensional

291

int frecuencia[] = new int[ 11 ]; // para cada calificación en LibroCalificaciones, incrementa la frecuencia apropiada for ( int califEstudiantes[] : calificaciones ) { for ( int calificacion : califEstudiantes ) ++frecuencia[ calificacion / 10 ]; } // fin de for exterior // para cada frecuencia de calificaciones, imprime una barra en el gráfico for ( int cuenta = 0; cuenta < frecuencia.length; cuenta++ ) { // imprime etiquetas de las barras ( "00-09: ", ..., "90-99: ", "100: " ) if ( cuenta == 10 ) System.out.printf( "%5d: ", 100 ); else System.out.printf( "%02d-%02d: ", cuenta * 10, cuenta * 10 + 9 ); // imprime barra de asteriscos for ( int estrellas = 0; estrellas < frecuencia[ cuenta ]; estrellas++ ) System.out.print( "*" ); System.out.println(); // inicia una nueva línea de salida } // fin de for exterior } // fin del método imprimirGraficoBarras // imprime el contenido del arreglo calificaciones public void imprimirCalificaciones() { System.out.println( "Las calificaciones son:\n" ); System.out.print( " " ); // alinea encabezados de columnas // crea un encabezado de columna para cada una de las pruebas for ( int prueba = 0; prueba < calificaciones[ 0 ].length; prueba++ ) System.out.printf( "Prueba %d ", prueba + 1 ); System.out.println( "Promedio" ); // encabezado de columna de promedio de estudiantes // crea filas/columnas de texto que representan el arreglo calificaciones for ( int estudiante = 0; estudiante < calificaciones.length; estudiante++ ) { System.out.printf( "Estudiante %2d", estudiante + 1 ); for ( int prueba : calificaciones[ estudiante ] ) // imprime calificaciones de estudiante System.out.printf( "%8d", prueba ); // llama al método obtenerPromedio para calcular la calificación promedio del estudiante; // pasa fila de calificaciones como argumento para obtenerPromedio double promedio = obtenerPromedio( calificaciones[ estudiante ] ); System.out.printf( "%9.2f\n", promedio ); } // fin de for exterior } // fin del método imprimirCalificaciones } // fin de la clase LibroCalificaciones

Figura 7.18 | Clase LibroCalificaciones que utiliza un arreglo bidimensional para almacenar calificaciones. (Parte 3 de 3).

www.elsolucionario.net

292

Capítulo 7 Arreglos

prueba y estudiante que empiecen en 1 (vea la figura 7.19) La instrucción for interna en las líneas 156 y 157 utiliza la variable contador estudiante de la instrucción for exterior para iterar a través de una fila específica del arreglo calificaciones, e imprime la calificación de la prueba de cada estudiante. Observe que una instrucción for mejorada puede anidarse en una instrucción for controlada por contador, y viceversa. Por último, la línea 161 obtiene el promedio semestral de cada estudiante, para lo cual pasa la fila actual de calificaciones (es decir, calificaciones[ estudiante ]) al método obtenerPromedio. El método obtenerPromedio (líneas 94 a 104) recibe un argumento: un arreglo unidimensional de resultados de la prueba para un estudiante específico. Cuando la línea 161 llama a obtenerPromedio, el argumento es calificaciones[ estudiante ], el cual especifica que debe pasarse una fila específica del arreglo bidimensional calificaciones a obtenerPromedio. Por ejemplo, con base en el arreglo creado en la figura 7.19, el argumento calificaciones[ 1 ] representa los tres valores (un arreglo unidimensional de calificaciones) almacenados en la fila 1 del arreglo bidimensional calificaciones. Recuerde que un arreglo bidimensional es un arreglo cuyos elementos son arreglos unidimensionales. El método obtenerPromedio calcula la suma de los elementos del arreglo, divide el total entre el número de resultados de la prueba y devuelve el resultado de punto flotante como un valor double (línea 103).

La clase PruebaLibroCalificaciones que demuestra la clase LibroCalificaciones La aplicación en la figura 7.19 crea un objeto de la clase LibroCalificaciones (figura 7.18) mediante el uso del arreglo bidimensional de valores int llamado arregloCalif (el cual se declara e inicializa en las líneas 10 a 19). Las líneas 21 y 22 pasan el nombre de un curso y arregloCalif al constructor de LibroCalificaciones. Después, las líneas 23 y 24 invocan a los métodos mostrarMensaje y procesarCalificaciones de miLibroCalificaciones, para mostrar un mensaje de bienvenida y obtener un informe que sintetice las calificaciones de los estudiantes para el semestre, respectivamente.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

// Fig. 7.19: PruebaLibroCalificaciones.java // Crea objeto LibroCalificaciones, usando un arreglo bidimensional de calificaciones. public class PruebaLibroCalificaciones { // el método main comienza la ejecución del programa public static void main( String args[] ) { // arreglo bidimensional de calificaciones de estudiantes int arregloCalif[][] = { { 87, 96, 70 }, { 68, 87, 90 }, { 94, 100, 90 }, { 100, 81, 82 }, { 83, 65, 85 }, { 78, 87, 65 }, { 85, 75, 83 }, { 91, 94, 100 }, { 76, 72, 84 }, { 87, 93, 73 } }; LibroCalificaciones miLibroCalificaciones = new LibroCalificaciones( "CS101 Introduccion a la programacion en Java", arregloCalif ); miLibroCalificaciones.mostrarMensaje(); miLibroCalificaciones.procesarCalificaciones(); } // fin de main } // fin de la clase PruebaLibroCalificaciones

Figura 7.19 | Crea un objeto LibroCalificaciones usando un arreglo bidimensional de calificaciones; después invoca al método procesarCalificaciones para analizarlas. (Parte 1 de 2).

www.elsolucionario.net

7.11

Listas de argumentos de longitud variable

293

Bienvenido al libro de calificaciones para CS101 Introduccion a la programacion en Java! Las calificaciones son: Estudiante Estudiante Estudiante Estudiante Estudiante Estudiante Estudiante Estudiante Estudiante Estudiante

1 2 3 4 5 6 7 8 9 10

Prueba 1 87 68 94 100 83 78 85 91 76 87

Prueba 2 96 87 100 81 65 87 75 94 72 93

Prueba 3 70 90 90 82 85 65 83 100 84 73

Promedio 84.33 81.67 94.67 87.67 77.67 76.67 81.00 95.00 77.33 84.33

La calificacion mas baja en el libro de calificaciones es 65 La calificacion mas alta en el libro de calificaciones es 100 Distribucion de calificaciones en general: 00-09: 10-19: 20-29: 30-39: 40-49: 50-59: 60-69: *** 70-79: ****** 80-89: *********** 90-99: ******* 100: ***

Figura 7.19 | Crea un objeto LibroCalificaciones usando un arreglo bidimensional de calificaciones; después invoca al método procesarCalificaciones para analizarlas. (Parte 2 de 2).

7.11 Listas de argumentos de longitud variable Con las listas de argumentos de longitud variable podemos crear métodos que reciben un número arbitrario de argumentos. Un tipo de argumento que va precedido por una elipsis (…) en la lista de parámetros de un método indica que éste recibe un número variable de argumentos de ese tipo específico. Este uso de la elipsis puede ocurrir sólo una vez en una lista de parámetros, y la elipsis, junto con su tipo, debe colocarse al final de la lista. Aunque los programadores pueden utilizar la sobrecarga de métodos y el paso de arreglos para realizar gran parte de lo que se logra con los “varargs” (listas de argumentos de longitud variable), es más conciso utilizar una elipsis en la lista de parámetros de un método. La figura 7.20 demuestra el método promedio (líneas 7 a 16), el cual recibe una secuencia de longitud variable de valores double. Java trata a la lista de argumentos de longitud variable como un arreglo cuyos elementos son del mismo tipo. Así, el cuerpo del método puede manipular el parámetro numeros como un arreglo de valores double. Las líneas 12 y 13 utilizan el ciclo for mejorado para recorrer el arreglo y calcular el total de los valores double en el arreglo. La línea 15 accede a numeros.length para obtener el tamaño del arreglo numeros y usarlo en el cálculo del promedio. Las líneas 29, 31 y 33 en main llaman al método promedio con dos, tres y cuatro argumentos, respectivamente. El método promedio tiene una lista de argumentos de longitud variable (línea 7), por lo que puede promediar todos los argumentos double que le pase el método que hace la llamada. La salida revela que cada llamada al método promedio devuelve el valor correcto.

Error común de programación 7.6 Colocar una elipsis en medio de una lista de parámetros de un método indicando una lista de argumentos de longitud variable es un error de sintaxis. La elipsis sólo debe colocarse al final de la lista de parámetros.

www.elsolucionario.net

294

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 d1 d2 d3 d4

Capítulo 7 Arreglos

// Fig. 7.20: PruebaVarargs.java // Uso de listas de argumentos de longitud variable. public class PruebaVarargs { // calcula el promedio public static double promedio( double... numeros ) { double total = 0.0; // inicializa el total // calcula el total usando la instrucción for mejorada for ( double d : numeros ) total += d; return total / numeros.length; } // fin del método promedio public static void main( String args[] ) { double d1 = 10.0; double d2 = 20.0; double d3 = 30.0; double d4 = 40.0; System.out.printf( "d1 = %.1f\nd2 = %.1f\nd3 = %.1f\nd4 = %.1f\n\n", d1, d2, d3, d4 ); System.out.printf( "El promedio de d1 y d2 es %.1f\n", promedio( d1, d2 ) ); System.out.printf( "El promedio de d1, d2 y d3 es %.1f\n", promedio( d1, d2, d3 ) ); System.out.printf( "El promedio de d1, d2, d3 y d4 es %.1f\n", promedio( d1, d2, d3, d4 ) ); } // fin de main } // fin de la clase PruebaVarargs = = = =

10.0 20.0 30.0 40.0

El promedio de d1 y d2 es 15.0 El promedio de d1, d2 y d3 es 20.0 El promedio de d1, d2, d3 y d4 es 25.0

Figura 7.20 | Uso de listas de argumentos de longitud variable.

7.12 Uso de argumentos de línea de comandos En muchos sistemas, es posible pasar argumentos desde la línea de comandos (a éstos se les conoce como argumentos de línea de comandos) a una aplicación, para lo cual se incluye un parámetro de tipo String[ ] (es decir, un arreglo de objetos String) en la lista de parámetros de main, justo igual que como hemos hecho en todas las aplicaciones de este libro. Por convención, a este parámetro se le llama args. Cuando se ejecuta una aplicación usando el comando java, Java pasa los argumentos de línea de comandos que aparecen después del nombre de la clase en el comando java al método main de la aplicación, en forma de objetos String en el arreglo args. El número de argumentos que se pasan desde la línea de comandos se obtiene accediendo al atributo length del arreglo. Por ejemplo, el comando "java miClase a b" pasa dos argumentos de línea de comandos, a y b, a la aplicación miClase. Observe que los argumentos de la línea de comandos se separan por espacio en blanco, no

www.elsolucionario.net

7.12

Uso de argumentos de línea de comandos

295

por comas. Cuando se ejecuta este comando, el método main de MiClase recibe el arreglo args de dos elementos (es decir, el valor del atributo length de args es 2), en el cual args[ 0 ] contiene el objeto String "a" y args [ 1 ] contiene el objeto String "b". Los usos comunes de los argumentos de línea de comandos incluyen pasar opciones y nombres de archivos a las aplicaciones. La figura 7.21 utiliza tres argumentos de línea de comandos para inicializar un arreglo. Cuando se ejecuta el programa, si args.length no es 3, el programa imprime un mensaje de error y termina (líneas 9 a 12). En cualquier otro caso, las líneas 14 a 32 inicializan y muestran el arreglo, con base en los valores de los argumentos de línea de comandos. Los argumentos de línea de comandos están disponibles para main como objetos String en args. La línea 16 obtiene args[ 0 ] (un objeto String que especifica el tamaño del arreglo) y lo convierte en un valor int, que el programa utiliza para crear el arreglo en la línea 17. El método static parseInt de la clase Integer convierte su argumento String en un int. Las líneas 20 a 21 convierten los argumentos de línea de comandos args[ 1 ] y args[ 2 ] en valores int, y los almacenan en valorInicial e incremento, respectivamente. Las líneas 24 y 25 calculan el valor para cada elemento del arreglo. Los resultados de la primera ejecución muestran que la aplicación recibió un número insuficiente de argumentos de línea de comandos. La segunda ejecución utiliza los argumentos de línea de comandos 5, 0 y 4 para especificar el tamaño del arreglo (5), el valor del primer elemento (0) y el incremento de cada valor en el arreglo (4), respectivamente. Los resultados correspondientes muestran que estos valores crean un arreglo que contiene los enteros 0, 4, 8, 12 y 16. Los resultados de la tercera ejecución muestran que los argumentos de línea de comandos 10, 1 y 2 producen un arreglo cuyos 10 elementos son los enteros impares positivos del 1 al 19.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

// Fig. 7.21: InicArreglo.java // Uso de los argumentos de línea de comandos para inicializar un arreglo. public class InicArreglo { public static void main( String args[] ) { // comprueba el número de argumentos de línea de comandos if ( args.length != 3 ) System.out.println( "Error: Vuelva a escribir el comando completo, incluyendo\n" + "el tamanio del arreglo, el valor inicial y el incremento." ); else { // obtiene el tamaño del arreglo del primer argumento de línea de comandos int longitudArreglo = Integer.parseInt( args[ 0 ] ); int arreglo[] = new int[ longitudArreglo ]; // crea el arreglo // obtiene el valor inicial y el incremento de los argumentos de línea de comandos int valorInicial = Integer.parseInt( args[ 1 ] ); int incremento = Integer.parseInt( args[ 2 ] ); // calcula el valor para cada elemento del arreglo for ( int contador = 0; contador < arreglo.length; contador++ ) arreglo[ contador ] = valorInicial + incremento * contador; System.out.printf( "%s%8s\n", "Indice", "Valor" ); // muestra el índice y el valor del arreglo for ( int contador = 0; contador < arreglo.length; contador++ ) System.out.printf( "%5d%8d\n", contador, arreglo[ contador ] );

Figura 7.21 | Inicialización de un arreglo, usando argumentos de línea de comandos. (Parte 1 de 2).

www.elsolucionario.net

296

32 33 34

Capítulo 7 Arreglos

} // fin de else } // fin de main } // fin de la clase InicArreglo

java InicArreglo Error: Vuelva a escribir el comando completo, incluyendo el tamanio del arreglo, el valor inicial y el incremento.

java InicArreglo 5 0 4 Indice Valor 0 0 1 4 2 8 3 12 4 16 java InicArreglo 10 1 2 Indice Valor 0 1 1 3 2 5 3 7 4 9 5 11 6 13 7 15 8 17 9 19

Figura 7.21 | Inicialización de un arreglo, usando argumentos de línea de comandos. (Parte 2 de 2).

7.13 (Opcional) Ejemplo práctico de GUI y gráficos: cómo dibujar arcos Mediante el uso de las herramientas para gráficos de Java, podemos crear dibujos complejos que, si los codificáramos línea por línea, sería un proceso tedioso. En las figuras 7.22 y 7.23 utilizamos arreglos e instrucciones de repetición para dibujar un arco iris, mediante el uso del método fillArc de Graphics. El proceso de dibujar arcos en Java es similar a dibujar óvalos; un arco es simplemente una sección de un óvalo. La figura 7.22 empieza con las instrucciones import usuales para ciertos dibujos (líneas 3 a 5). Las líneas 9 y 10 declaran y crean dos nuevos colores: VIOLETA e INDIGO. Como tal vez lo sepa, los colores de un arco iris son rojo, naranja, amarillo, verde, azul, índigo y violeta. Java tiene constantes predefinidas sólo para los primeros cinco colores. Las líneas 15 a 17 inicializan un arreglo con los colores del arco iris, empezando con los arcos más interiores primero. El arreglo empieza con dos elementos Color.WHITE, que como veremos pronto, son para dibujar los arcos vacíos en el centro del arco iris. Observe que las variables de instancia se pueden inicializar al momento de declararse, como se muestra en las líneas 10 a 17. El constructor (líneas 20 a 23) contiene una sola instrucción que llama al método setBackground (heredado de la clase JPanel) con el parámetro Color.WHITE. El método setBackground recibe un solo argumento Color y establece el color de fondo del componente a ese color. La línea 30 en paintComponent declara la variable local radio, que determina el radio de cada arco. Las variables locales centroX y centroY (líneas 33 y 34) determinan la ubicación del punto medio en la base del arco iris. El ciclo en las líneas 37 a 46 utiliza la variable de control contador para contar en forma regresiva, partiendo del final del arreglo, dibujando los arcos más grandes primero y colocando cada arco más pequeño encima del anterior. La línea 40 establece el color para dibujar el arco actual del arreglo. La razón por la que tenemos entradas Color.WHITE al principio del arreglo es para crear el arco vacío en el centro. De no ser así, el centro del arco iris

www.elsolucionario.net

7.13

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

(Opcional) Ejemplo práctico de GUI y gráficos: cómo dibujar arcos

297

// Fig. 7.22: DibujoArcoIris.java // Demuestra el uso de colores en un arreglo. import java.awt.Color; import java.awt.Graphics; import javax.swing.JPanel; public class DibujoArcoIris extends JPanel { // Define los colores índigo y violeta final Color VIOLETA = new Color( 128, 0, 128 ); final Color INDIGO = new Color( 75, 0, 130 ); // los colores a usar en el arco iris, empezando desde los más interiores // Las dos entradas de color blanco producen un arco vacío en el centro private Color colores[] = { Color.WHITE, Color.WHITE, VIOLETA, INDIGO, Color.BLUE, Color.GREEN, Color.YELLOW, Color.ORANGE, Color.RED }; // constructor public DibujoArcoIris() { setBackground( Color.WHITE ); // establece el fondo al color blanco } // fin del constructor de DibujoArcoIris // dibuja un arco iris, usando círculos concéntricos public void paintComponent( Graphics g ) { super.paintComponent( g ); int radio = 20; // el radio de un arco // dibuja el arco iris cerca de la parte central inferior int centroX = getWidth() / 2; int centroY = getHeight() - 10; // dibuja arcos rellenos, empezando con el más exterior for ( int contador = colores.length; contador > 0; contador-- ) { // establece el color para el arco actual g.setColor( colores[ contador - 1 ] ); // rellena el arco desde 0 hasta 180 grados g.fillArc( centroX - contador * radio, centroY - contador * radio, contador * radio * 2, contador * radio * 2, 0, 180 ); } // fin de for } // fin del método paintComponent } // fin de la clase DibujoArcoIris

Figura 7.22 | Dibujo de un arco iris, usando arcos y un arreglo de colores.

sería un semicírculo sólido color violeta. [Nota: puede cambiar los colores individuales y el número de entradas en el arreglo para crear nuevos diseños]. La llamada al método fillArc en las líneas 43 a 45 dibuja un semicírculo relleno. El método fillArc requiere seis parámetros. Los primeros cuatro representan el rectángulo delimitador en el cual se dibujará el arco. Los primeros dos de estos cuatro especifican las coordenadas para la esquina superior izquierda del rectángulo delimitador, y los siguientes dos especifican su anchura y su altura. El quinto parámetro es el ángulo inicial en el óvalo, y el sexto especifica el barrido o la cantidad de arco que se cubrirá. El ángulo inicial y el barrido se miden en

www.elsolucionario.net

298

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

Capítulo 7 Arreglos

// Fig. 7.23: DrawRainbowTest.java // Aplicación de prueba para mostrar un arco iris. import javax.swing.JFrame; public class PruebaDibujoArcoIris { public static void main( String args[] ) { DibujoArcoIris panel = new DibujoArcoIris(); JFrame aplicacion = new JFrame(); aplicacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); aplicacion.add( panel ); aplicacion.setSize( 400, 250 ); aplicacion.setVisible( true ); } // fin de main } // fin de la clase PruebaDibujoArcoIris

Figura 7.23 | Creación de un objeto JFrame para mostrar un arco iris.

grados, en donde los cero grados apuntan a la derecha. Un barrido positivo dibuja el arco en sentido contrario a las manecillas del reloj, mientras que un barrido negativo dibuja el arco en sentido de las manecillas del reloj. Un método similar a fillArc es drawArc; requiere los mismos parámetros que fillArc, pero dibuja el borde del arco, en vez de rellenarlo. La clase PruebaDibujoArcoIris (figura 7.23) crea y establece un objeto JFrame para mostrar el arco iris en la pantalla. Una vez que el programa hace visible el objeto JFrame, el sistema llama al método paintComponent en la clase DibujoArcoIris para dibujar el arco iris en la pantalla.

Ejercicio del ejemplo práctico de GUI y gráficos 7.1

(Dibujo de espirales) En este ejercicio, dibujará espirales con los métodos drawLine y drawArc. a) Dibuje una espiral con forma cuadrada (como en la captura de pantalla izquierda de la figura 7.24), centrada en el panel, usando el método drawLine. Una técnica es utilizar un ciclo que incremente la longitud de la línea después de dibujar cada segunda línea. La dirección en la cual se dibujará la siguiente línea debe ir después de un patrón distinto, como abajo, izquierda, arriba, derecha. b) Dibuje una espiral circular (como en la captura de pantalla derecha de la figura 7.24), usando el método drawArc para dibujar un semicírculo a la vez. Cada semicírculo sucesivo deberá tener un radio más grande (según lo especificado mediante la anchura del rectángulo delimitador) y debe seguir dibujando en donde terminó el semicírculo anterior.

www.elsolucionario.net

7.14

(Opcional) Ejemplo práctico de Ingeniería de Software: colaboración entre los objetos

299

Figura 7.24 | Dibujo de una espiral usando drawLine (izquierda) y drawArc (derecha).

7.14 (Opcional) Ejemplo práctico de Ingeniería de Software: colaboración entre los objetos 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 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 6.14 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 colaboraciones en el sistema, regresaremos al documento de requerimientos de la sección 2.9. 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 ésta y las siguientes secciones del Ejemplo práctico de Ingeniería de Software que quedan en el libro, es probable que descubramos relaciones adicionales.

Identificar las colaboraciones en un sistema Para 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 receptor (identificadas en la sección 6.14) 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 colaboració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, precedido por un artículo (por ejemplo, “un”, “una”, “el” o “la”); por ejemplo, “el ATM” hace referencia a un objeto de la clase ATM]. La figura 7.25 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 ejemplo, la primera fila en la figura 7.25 indica que el objeto ATM colabora con el objeto Pantalla cada vez que el ATM necesita mostrar un mensaje al usuario.

www.elsolucionario.net

300

Capítulo 7 Arreglos

Un objeto de la clase…

envía el mensaje…

a un objeto de la clase…

ATM

mostrarMensaje

Pantalla

obtenerEntrada

Teclado

autenticarUsuario

BaseDatosBanco

ejecutar

SolicitudSaldo

ejecutar

Retiro

ejecutar

Deposito

obtenerSaldoDisponible

BaseDatosBanco

obtenerSaldoTotal

BaseDatosBanco

mostrarMensaje

Pantalla

MostrarMensaje

Pantalla

obtenerEntrada obtenerSaldoDisponible

Teclado BaseDatosBanco

haySuficienteEfectivoDisponible

DispensadorEfectivo

cargar

BaseDatosBanco

dispensarEfectivo

DispensadorEfectivo

mostrarMensaje obtenerEntrada seRecibioSobreDeposito abonar

Pantalla

validarNIP obtenerSaldoDisponible obtenerSaldoTotal cargar abonar

Cuenta Cuenta Cuenta Cuenta Cuenta

SolicitudSaldo

Retiro

Deposito

BaseDatosBanco

Teclado RanuraDeposito BaseDatosBanco

Figura 7.25 | Colaboraciones en el sistema ATM.

Consideraremos las colaboraciones en la figura 7.25. 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 7.25. El ATM obtiene la entrada en respuesta a un indicador, mediante el envío de un mensaje obtenerEntrada del Teclado. A continuación, el ATM debe determinar si el número de cuenta especificado 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 7.25 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 mensajes 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 colaboraciones, por lo que no agregamos nada a la figura 7.25. 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 ejemplo, si el usuario elije realizar una solicitud de saldo, el ATM envía un mensaje ejecutar a un objeto SolicitudSaldo. 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 BaseDatosBanco, 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

www.elsolucionario.net

7.14

(Opcional) Ejemplo práctico de Ingeniería de Software: colaboración entre los objetos

301

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 objeto 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 de la cuenta del usuario. Para obtener el monto de dinero disponible en la cuenta del usuario, el objeto 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. Recuerde que al hacer un cargo a una Cuenta se reduce tanto el saldo total 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 mensaje 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 incrementa el saldoTotal, pero no el saldoDisponible.

Diagramas de interacción Ahora 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 objetos interactúan entre sí. El diagrama de comunicación enfatiza cuáles objetos participan en las colaboraciones. [Nota: en versiones anteriores de UML los diagramas de comunicación se llamaban diagramas de colaboración]. Al igual que el diagrama de comunicación, el diagrama de secuencia muestra las colaboraciones entre los objetos, pero enfatiza cuándo se deben enviar los mensajes entre los objetos a través del tiempo.

Diagramas de comunicación La figura 7.26 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 forma nombreObjeto : NombreClase. En este ejemplo, que involucra sólo a un objeto de cada tipo, descartamos 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 cuando 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”). La flecha rellena en la figura 7.26 representa un mensaje (o llamada síncrona) en UML y una llamada a un método en Java. Esta flecha indica que el flujo de control va desde el objeto emisor (el ATM) 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

ejecutar() : ATM

: SolicitudSaldo

Figura 7.26 | Diagrama de comunicación del ATM, ejecutando una solicitud de saldo.

www.elsolucionario.net

302

Capítulo 7 Arreglos

emisor. El emisor sólo espera. Por ejemplo, en la figura 7.26 el objeto ATM llama al método ejecutar de un objeto SolicitudSaldo y no puede enviar otro mensaje sino hasta que ejecutar termine y devuelva el control al objeto ATM. [Nota: si ésta fuera una llamada asíncrona, representada 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 subprocesamiento múltiple, que veremos en el capítulo 23, Subprocesamiento múltiple].

Secuencia de mensajes en un diagrama de comunicación La figura 7.27 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 numeroCuenta del objeto contiene el número de cuenta del usuario actual. Las colaboraciones en la figura 7.27 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 7.26). El número a la izquierda del nombre de un mensaje 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 obtenerSaldoDisponible al objeto BaseDatosBanco (mensaje 1), después envía un mensaje obtenerSaldoTotal 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 indicar de cuál objeto Cuenta se extraerá la información del saldo. En la figura 6.22 vimos que las operaciones obtenerSaldoDisponible y obtenerSaldoTotal de la clase BaseDatosBanco requieren cada una de ellas un parámetro para identificar una cuenta. El objeto SolicitudSaldo muestra a continuació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. Observe que la figura 7.27 modela dos mensajes adicionales que se pasan del objeto BaseDatosBanco a un objeto 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 obtenerSaldoDisponible 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 esquema de numeración decimal para indicar mensajes anidados. Por ejemplo, el mensaje 1.1 es el primer mensaje anidado en el mensaje 1; el objeto BaseDatosBanco pasa un mensaje obtenerSaldoDisponible durante el procesamiento de BaseDatosBanco

: Pantalla

3: mostrarMensaje( mensaje )

: SolicitudSaldo

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

: BaseDatosBanco

: Cuenta

1.1: obtenerSaldoDisponible() 2.1: obtenerSaldoTotal()

Figura 7.27 | Diagrama de comunicación para ejecutar una solicitud de saldo.

www.elsolucionario.net

7.14

(Opcional) Ejemplo práctico de Ingeniería de Software: colaboración entre los objetos

303

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 mensajes 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 aclarar con precisión cuándo y en qué contexto se pasa cada mensaje. Por ejemplo, si numeramos los cinco mensajes de la figura 7.27 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 mensaje 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 obtenerSaldoDisponible (mensaje 1) por parte del objeto BaseDatosBanco.

Diagramas de secuencia Los 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 sincronización de las colaboraciones con más claridad. La figura 7.28 muestra un diagrama de secuencia que modela la secuencia de las interacciones que ocurren cuando se ejecuta un Retiro. La línea punteada 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 objeto, 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 diagramas 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 activación en la línea de vida del objeto receptor. Una activación, que se muestra como un rectángulo vertical 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ó originalmente 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 comunicació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 7.28 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 5.31 modelamos 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 7.28. 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 contiene un monto de efectivo suficiente como para satisfacer la solicitud. Para obtener información acerca de cómo modelar la lógica de control en un diagrama de secuencia, consulte los recursos Web y las lecturas recomendadas que se listan al final de la sección 2.9. Después de obtener un monto de retiro, el objeto Retiro envía un mensaje obtenerSaldoDisponible 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 permitir la transacción, el objeto Retiro envía al objeto DispensadorEfectivo un mensaje haySuficienteEfectivoDisponible. 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 mensaje cargar a la Cuenta del usuario. Por último, el objeto Retiro envía al DispensadorEfectivo un mensaje dispensarEfectivo y a la Pantalla un mensaje mostrarMensaje, indicando al usuario que retire 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 siguiente sección del Ejemplo práctico de Ingeniería de Software (sección 8.19), 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.

www.elsolucionario.net

304

Capítulo 7 Arreglos

: Retiro

: Teclado

: Pantalla

: Cuenta

: BaseDatosBanco

: DispensadorEfectivo

mostrarMensaje( mensaje )

obtenerEntrada()

obtenerSaldoDisponible( numeroCuenta ) obtenerSaldoDisponible()

haySuficienteEfectivoDisponible( monto )

cargar( numeroCuenta, monto ) cargar( monto )

dispensarEfectivo( monto )

mostrarMensaje( mensaje )

Figura 7.28 | Diagrama de secuencia que modela la ejecución de un Retiro.

Ejercicios de autoevaluación del Ejemplo práctico de Ingeniería de Software 7.1

Un(a) __________ consiste en que un objeto de una clase envía un mensaje a un objeto de otra clase. a) asociación b) agregación c) colaboración d) composición

7.2 ¿Cuál forma de diagrama de interacción es la que enfatiza qué colaboraciones se llevan a cabo? ¿Cuál forma enfatiza cuándo ocurren las interacciones? 7.3 Cree un diagrama de secuencia para modelar las interacciones entre los objetos del sistema ATM, que ocurran cuando se ejecute un Deposito con éxito. Explique la secuencia de los mensajes modelados por el diagrama.

Respuestas a los ejercicios de autoevaluación del Ejemplo práctico de Ingeniería de Software 7.1 c. 7.2 Los diagramas de comunicación enfatizan qué colaboraciones se llevan a cabo. Los diagramas de secuencia enfatizan cuándo ocurren las colaboraciones.

www.elsolucionario.net

7.15

Conclusión

305

7.3 La figura 7.29 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. La figura 7.29 indica que un Deposito primero 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 mensaje 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.

: Deposito

: Teclado

: Pantalla

: BaseDatosBanco

: RanuraDeposito

: Cuenta

mostrarMensaje( mensaje )

obtenerEntrada()

mostrarMensaje( mensaje )

seRecibioSobreDeposito()

abonar( numeroCuenta, monto ) abonar( monto )

Figura 7.29 | Diagrama de secuencia que modela la ejecución de un Deposito.

7.15 Conclusión En este capítulo empezó nuestra introducción a las estructuras de datos, explorando el uso de los arreglos para almacenar datos y obtenerlos de listas y tablas de valores. Los ejemplos de este capítulo demostraron cómo declarar un arreglo, inicializarlo y hacer referencia a los elementos individuales de un arreglo. Se introdujo la instrucción for mejorada para iterar a través de los arreglos. También le mostramos cómo pasar arreglos a los métodos, y cómo declarar y manipular arreglos multidimensionales. Por último, en este capítulo se demostró cómo escribir métodos que utilizan listas de argumentos de longitud variable, y cómo leer argumentos que se pasan a un programa desde la línea de comandos. Continuaremos con nuestra cobertura de las estructuras de datos en el capítulo 17, Estructuras de datos, que introduce las estructuras de datos dinámicas, como listas, colas, pilas y árboles, que pueden crecer y reducirse a medida que se ejecutan los programas. En el capítulo 18, Genéricos, se presenta el tema de los genéricos, que proporcionan los medios para crear modelos generales de métodos y clases que pueden declararse una vez, pero

www.elsolucionario.net

306

Capítulo 7 Arreglos

utilizarse con muchos tipos de datos distintos. En el capítulo 19, Colecciones, se introduce el Java Collections Framework (Marco de trabajo de colecciones de Java), que utiliza los genéricos para permitir a los programadores especificar los tipos exactos de objetos que almacenará una estructura de datos específica. En el capítulo 19 también se introducen las estructuras de datos predefinidas de Java, que los programadores pueden usar en vez de tener que construir sus propias estructuras. El capítulo 19 habla sobre muchas clases de estructuras de datos, incluyendo Vector y ArrayList, que son estructuras de datos similares a los arreglos y pueden aumentar y reducir su tamaño en respuesta al cambio en los requerimientos de almacenamiento de un programa. La API Collections también proporciona la clase Arrays, que contiene métodos utilitarios para la manipulación de arreglos. El capítulo 19 utiliza varios métodos static de la clase Arrays para realizar manipulaciones como ordenar y buscar en los datos de un arreglo. Después de leer este capítulo, usted podrá utilizar algunos de los métodos de Arrays que se describen en el capítulo 19, pero algunos de los métodos de Arrays requieren un conocimiento sobre los conceptos que presentaremos más adelante en este libro. Ya le hemos presentado los conceptos básicos de las clases, los objetos, las instrucciones de control, los métodos y los arreglos. En el capítulo 8 analizaremos con más detalle las clases y los objetos.

Resumen Sección 7.1 Introducción • Los arreglos son estructuras de datos que consisten en elementos de datos relacionados del mismo tipo; son entidades de longitud fija; permanecen con la misma longitud una vez que se crean, aunque a una variable tipo arreglo se le puede reasignar la referencia de un nuevo arreglo de una longitud distinta.

Sección 7.2 Arreglos • Un arreglo es un grupo de variables (llamadas elementos o componentes) que contienen valores, todos con el mismo tipo. Los arreglos son objetos, por lo cual se consideran como tipos por referencia. Los elementos de un arreglo pueden ser tipos primitivos o tipos por referencia (incluyendo arreglos). • Para hacer referencia a un elemento específico en un arreglo, especificamos el nombre de la referencia al arreglo y el índice (subíndice) del elemento en el arreglo. • Un programa hace referencia a cualquiera de los elementos de un arreglo mediante una expresión de acceso a un arreglo, la cual incluye el nombre del arreglo, seguido del subíndice del elemento específico entre corchetes ([]). • El primer elemento en cada arreglo tiene el subíndice cero, y algunas veces se le llama el elemento cero. • Un subíndice debe ser un entero positivo. Un programa puede utilizar una expresión como un subíndice. • Todo objeto tipo arreglo conoce su propia longitud, y mantiene esta información en un campo length. La expresión arreglo.length accede al campo length de arreglo, para determinar la longitud del arreglo.

Sección 7.3 Declaración y creación de arreglos • Para crear un objeto tipo arreglo, el programador especifica el tipo de los elementos del arreglo y el número de elementos como parte de una expresión de creación de arreglo, que utiliza la palabra clave new. La siguiente expresión de creación de arreglo crea un arreglo de 100 valores int: int b[ ] = new int[ 100 ];

• Cuando se crea un arreglo, cada elemento del mismo recibe un valor predeterminado: cero para los elementos numéricos de tipo primitivo, false para los elementos booleanos y null para las referencias (cualquier tipo no primitivo). • Cuando se declara un arreglo, su tipo y los corchetes pueden combinarse al principio de la declaración, para indicar que todos los identificadores en la declaración son variables tipo arreglo, como en double[ ] arreglo1, arreglo2;

• Un programa puede declarar arreglos de cualquier tipo. Cada elemento de un arreglo de tipo primitivo contiene una variable del tipo declarado del arreglo. De manera similar, en un arreglo de un tipo por referencia, cada elemento es una referencia a un objeto del tipo declarado del arreglo.

www.elsolucionario.net

Resumen

307

Sección 7.4 Ejemplos acerca del uso de los arreglos • Un programa puede crear un arreglo e inicializar sus elementos con un inicializador de arreglos (es decir, una lista inicializadora encerrada entre llaves). • Las variables constantes (también conocidas como constantes con nombre o variables de sólo lectura) deben inicializarse antes de utilizarlas, y no pueden modificarse de ahí en adelante. • Cuando se ejecuta un programa en Java, la JVM comprueba los subíndices de los arreglos para asegurarse que sean válidos (es decir, deben ser mayores o iguales a 0 y menores que la longitud del arreglo). Si un programa utiliza un subíndice inválido, Java genera algo que se conoce como excepción, para indicar que ocurrió un error en el programa, en tiempo de ejecución.

Sección 7.6 Instrucción for mejorada • La instrucción for mejorada permite a los programadores iterar a través de los elementos de un arreglo o de una colección, sin utilizar un contador. La sintaxis de una instrucción for mejorada es: parámetro : nombreArreglo instrucción

for (

)

en donde parámetro tiene dos partes: un tipo y un identificador (por ejemplo, int numero), y nombreArreglo es el arreglo a través del cual se iterará. • La instrucción for mejorada no puede usarse para modificar los elementos de un arreglo. Si un programa necesita modificar elementos, use la instrucción for tradicional, controlada por contador.

Sección 7.7 Paso de arreglos a los métodos • Cuando un argumento se pasa por valor, se hace una copia del valor del argumento y se pasa al método que se llamó. Este método trabaja exclusivamente con la copia. • Cuando se pasa un argumento por referencia, el método al que se llamó puede acceder al valor del argumento en el método que lo llamó directamente, y es posible que pueda modificarlo. • Java no permite a los programadores elegir entre el paso por valor y el paso por referencia; todos los argumentos se pasan por valor. Una llamada a un método puede pasar dos tipos de valores a un método: copias de valores primitivos (por ejemplo, valores de tipo int y double) y copias de referencias a objetos. Aunque la referencia a un objeto se pasa por valor, un método de todas formas puede interactuar con el objeto referenciado, llamando a sus métodos public mediante el uso de la copia de la referencia al objeto. • Para pasar a un método una referencia a un objeto, simplemente se especifica en la llamada al método el nombre de la variable que hace referencia al objeto. • Cuando un argumento para un método es todo un arreglo, o un elemento individual del arreglo de tipo por referencia, el método que se llamó recibe una copia del arreglo o referencia al elemento. Cuando un argumento para un método es un elemento individual de un arreglo de tipo primitivo, el método que se llamó recibe una copia del valor del elemento. • Para pasar un elemento individual del arreglo a un método, use el nombre indexado del arreglo como argumento en la llamada al método.

Sección 7.9 Arreglos multidimensionales • Los arreglos multidimensionales con dos dimensiones se utilizan a menudo para representar tablas de valores, que consisten en información ordenada en filas y columnas. • Los arreglos que requieren dos subíndices para identificar un elemento específico se llaman arreglos bidimensionales. Un arreglo con m filas y n columnas se llama arreglo de m por n. Un arreglo bidimensional puede inicializarse con un inicializador de arreglos, de la forma tipoArreglo nombreArreglo[][]

= { {

fila1 inicializador

}, {

fila2 inicializador }, … };

• Los arreglos multidimensionales se mantienen como arreglos de arreglos unidimensionales separados. Como resultado, no es obligatorio que las longitudes de las filas en un arreglo bidimensional sean iguales. • Un arreglo multidimensional con el mismo número de columnas en cada fila se puede crear mediante una expresión de creación de arreglos, de la forma tipoArreglo nombreArreglo[][]

= new

tipoArreglo[ numFilas ][ numColumnas ];

• Un tipo de argumento seguido por una elipsis (…) en la lista de parámetros de un método indica que éste recibe un número variable de argumentos de ese tipo específico. La elipsis puede ocurrir sólo una vez en la lista de parámetros de un método, y debe estar al final de la lista.

www.elsolucionario.net

308

Capítulo 7 Arreglos

Sección 7.11 Listas de argumentos de longitud variable • Una lista de argumentos de longitud variable se trata como un arreglo dentro del cuerpo del método. El número de argumentos en el arreglo se puede obtener mediante el campo length del arreglo.

Sección 7.12 Uso de argumentos de línea de comandos • Para pasar argumentos a main en una aplicación de Java, desde la línea de comandos, se incluye un parámetro de tipo String[ ] en la lista de parámetros de main. Por convención, el parámetro de main se llama args. • Java pasa los argumentos de línea de comandos que aparecen después del nombre de la clase en el comando java al método main de la aplicación, en forma de objetos String en el arreglo args. El número de argumentos que se pasan de la línea de comandos se obtiene accediendo al atributo length del arreglo.

Terminología 0, bandera (en un especificador de formato) a[ i ] a[ i ][ j ]

argumentos de línea de comandos arreglo arreglo bidimensional arreglo de m por n arreglo multidimensional arreglo unidimensional cantidad escalar columna de un arreglo bidimensional componente de un arreglo comprobación de límites constante con nombre corchetes, [] declarar un arreglo declarar una variable constante elemento de un arreglo elipsis (…) en la lista de parámetros de un método error de desplazamiento por uno escalar estructura de datos expresión de acceso a un arreglo expresión de creación de arreglos fila de un arreglo bidimensional final, palabra clave

formato tabular índice índice de columna inicializador de arreglos inicializadores de arreglos anidados inicializar un arreglo instrucción for mejorada length, campo de un arreglo lista de argumentos de longitud variable lista inicializadora llamada por referencia llamada por valor nombre de un arreglo número de posición parseInt, método de la clase Integer paso de arreglos a métodos paso por referencia paso por valor recorrer un arreglo subíndice subíndice cero subíndice de fila tabla de valores valor de un elemento variable constante variable de sólo lectura

Ejercicios de autoevaluación 7.1

Complete las siguientes oraciones: a) Las listas y tablas de valores pueden guardarse en ____________. b) Un arreglo es un grupo de ____________ (llamados elementos o componentes) que contiene valores, todos con el mismo ____________. c) La ____________ permite a los programadores iterar a través de los elementos en un arreglo, sin utilizar un contador. d) El número utilizado para referirse a un elemento específico de un arreglo se conoce como el ___________ ______ de ese elemento. e) Un arreglo que utiliza dos subíndices se conoce como un arreglo ____________. f ) Use la instrucción for mejorada ____________ para recorrer el arreglo double llamado numeros. g) Los argumentos de línea de comandos se almacenan en ____________. h) Use la expresión ____________ para recibir el número total de argumentos en una línea de comandos. Suponga que los argumentos de línea de comandos se almacenan en el objeto String args[].

www.elsolucionario.net

Respuestas a los ejercicios de autoevaluación

309

i) Dado el comando java MiClase prueba, el primer argumento de línea de comandos es ____________. j) Un(a) _____________________ en la lista de parámetros de un método indica que el método puede recibir un número variable de argumentos. 7.2

Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué. a) Un arreglo puede guardar muchos tipos distintos de valores. b) El índice de un arreglo debe ser generalmente de tipo float. c) Un elemento individual de un arreglo que se pasa a un método y se modifica ahí mismo, contendrá el valor modificado cuando el método llamado termine su ejecución. d) Los argumentos de línea de comandos se separan por comas.

7.3

Realice las siguientes tareas para un arreglo llamado fracciones: a) Declare una constante llamada TAMANIO_ARREGLO que se inicialice con 10. b) Declare un arreglo con TAMANIO_ARREGLO elementos de tipo double, e inicialice los elementos con 0. c) Haga referencia al elemento 4 del arreglo. d) Asigne el valor 1.667 al elemento 9 del arreglo. e) Asigne el valor 3.333 al elemento 6 del arreglo. f ) Sume todos los elementos del arreglo, utilizando una instrucción for. Declare la variable entera x como variable de control para el ciclo.

7.4

Realice las siguientes tareas para un arreglo llamado tabla: a) Declare y cree el arreglo como un arreglo entero con tres filas y tres columnas. Suponga que se ha declarado la constante TAMANIO_ARREGLO con el valor de 3. b) ¿Cuántos elementos contiene el arreglo? c) Utilice una instrucción for para inicializar cada elemento del arreglo con la suma de sus índices. Suponga que se declaran las variables enteras x y y como variables de control.

7.5

Encuentre y corrija el error en cada uno de los siguientes fragmentos de programa: a) final int TAMANIO_ARREGLO = 5; TAMANIO_ARREGLO = 10; b) Suponga que int b[] = new int[ 10 ]; for ( int i = 0; i = 0 && s < 60 ) ? s : 0 ); // valida el segundo } // fin del método establecerTiempo // convierte a objeto String en formato de hora universal (HH:MM:SS) public String aStringUniversal() { return String.format( "%02d:%02d:%02d", hora, minuto, segundo ); } // fin del método aStringUniversal // convierte a objeto String en formato de hora estándar (H:MM:SS AM o PM) public String toString() { return String.format( "%d:%02d:%02d %s", ( ( hora == 0 || hora == 12 ) ? 12 : hora % 12 ), minuto, segundo, ( hora < 12 ? "AM" : "PM" ) ); } // fin del método toString } // fin de la clase Tiempo1

Figura 8.1 | La declaración de la clase Tiempo1 mantiene la hora en formato de 24 horas.

www.elsolucionario.net

328

Capítulo 8

Clases y objetos: un análisis más detallado

los valores de minuto y segundo (líneas 15 y 16) deben ser mayores o iguales que 0 y menores que 60. Cualquier valor fuera de estos rangos se establece como cero para asegurar que un objeto Tiempo1 siempre contenga datos consistentes; esto es, los valores de datos del objeto siempre se mantienen en rango, aun si los valores que se proporcionan como argumentos para el método establecerTiempo son incorrectos. En este ejemplo, cero es un valor consistente para hora, minuto y segundo. Un valor que se pasa a establecerTiempo es correcto si se encuentra dentro del rango permitido para el miembro que va a inicializar. Por lo tanto, cualquier número en el rango de 0 a 23 sería un valor correcto para la hora. Un valor correcto siempre es un valor consistente. Sin embargo, un valor consistente no es necesariamente un valor correcto. Si establecerTiempo establece hora a 0 debido a que el argumento que recibió se encontraba fuera del rango, entonces establecerTiempo está recibiendo un valor incorrecto y lo hace consistente, para que el objeto permanezca en un estado consistente en todo momento. La hora correcta del día podrían ser las 11 AM, pero debido a que la persona pudo haber introducido en forma accidental una hora fuera de rango (incorrecta), optamos por establecer la hora al valor consistente de cero. En este caso, tal vez sea conveniente indicar que el objeto es incorrecto. En el capítulo 13, Manejo de excepciones, aprenderá técnicas elegantes que permitirán a sus clases indicar cuándo se reciben valores incorrectos.

Observación de ingeniería de software 8.1 Los métodos que modifican los valores de variables private deben verificar que los nuevos valores que se les pretende asignar sean apropiados. Si no lo son, deben colocar las variables private en un estado consistente apropiado.

El método aStringUniversal (líneas 20 a 23) no recibe argumentos y devuelve un objeto String en formato de hora universal, el cual consiste de seis dígitos: dos para la hora, dos para los minutos y dos para los segundos. Por ejemplo, si la hora es 1:30:07 PM, el método aStringUniversal devuelve 13:30:07. La instrucción return (línea 22) utiliza el método static format de la clase String para devolver un objeto String que contiene los valores con formato de hora, minuto y segundo, cada uno con dos dígitos y posiblemente, un 0 a la izquierda (el cual se especifica con la bandera 0). El método format es similar al método System.out.printf, sólo que format devuelve un objeto String con formato, en vez de mostrarlo en una ventana de comandos. El método aStringUniversal devuelve el objeto String con formato. El método toString (líneas 26 a 31) no recibe argumentos y devuelve un objeto String en formato de hora estándar, el cual consiste en los valores de hora, minuto y segundo separados por signos de dos puntos (:), y seguidos de un indicador AM o PM (por ejemplo, 1:27:06 PM). Al igual que el método aStringUniversal, el método toString utiliza el método static String format para dar formato a los valores de minuto y segundo como valores de dos dígitos con 0s a la izquierda, en caso de ser necesario. La línea 29 utiliza un operador condicional (?:) para determinar el valor de hora en la cadena; si hora es 0 o 12 (AM o PM), aparece como 12; en cualquier otro caso, aparece como un valor de 1 a 11. El operador condicional en la línea 30 determina si se devolverá AM o PM como parte del objeto String. En la sección 6.4 vimos que todos los objetos en Java tienen un método toString que devuelve una representación String del objeto. Optamos por devolver un objeto String que contiene la hora en formato estándar. El método toString se puede llamar en forma implícita cada vez que aparece un objeto Tiempo1 en el código, en donde se necesita un String, como el valor para imprimir con un especificador de formato %s en una llamada a System.out.printf.

Uso de la clase Tiempo1 Como aprendió en el capítulo 3, cada clase que se declara representa un nuevo tipo en Java. Por lo tanto, después de declarar la clase Tiempo1, podemos utilizarla como un tipo en las declaraciones como Tiempo1

puestasol; // puestasol puede guardar una referencia a un objeto Tiempo1

La clase de la aplicación PruebaTiempo1 (figura 8.2) utiliza la clase Tiempo1. La línea 9 declara y crea un objeto Tiempo1 y lo asigna a la variable local tiempo. Observe que new invoca en forma implícita al constructor predeterminado de la clase Tiempo1, ya que Tiempo1 no declara constructores. Las líneas 12 a 16 imprimen en pantalla la hora, primero en formato universal (mediante la invocación al método aStringUniversal en la línea 13) y después en formato estándar (mediante la invocación explícita del método toString de tiempo en la línea 15) para confirmar que el objeto Tiempo1 se haya inicializado en forma apropiada.

www.elsolucionario.net

8.2

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

Ejemplo práctico de la clase Tiempo

329

// Fig. 8.2: PruebaTiempo1.java // Objeto Tiempo1 utilizado en una aplicación. public class PruebaTiempo1 { public static void main( String args[] ) { // crea e inicializa un objeto Tiempo1 Tiempo1 tiempo = new Tiempo1(); // invoca el constructor de Tiempo1 // imprime representaciones de cadena del tiempo System.out.print( "La hora universal inicial es: " ); System.out.println( tiempo.aStringUniversal() ); System.out.print( "La hora estandar inicial es: " ); System.out.println( tiempo.toString() ); System.out.println(); // imprime una línea en blanco // modifica el tiempo e imprime el tiempo actualizado tiempo.establecerTiempo( 13, 27, 6 ); System.out.print( "La hora universal despues de establecerTiempo es: " ); System.out.println( tiempo.aStringUniversal() ); System.out.print( "La hora estandar despues de establecerTiempo es: " ); System.out.println( tiempo.toString() ); System.out.println(); // imprime una línea en blanco // establece el tiempo con valores inválidos; imprime el tiempo actualizado tiempo.establecerTiempo( 99, 99, 99 ); System.out.println( "Despues de intentar ajustes invalidos:" ); System.out.print( "Hora universal: " ); System.out.println( tiempo.aStringUniversal() ); System.out.print( "Hora estandar: " ); System.out.println( tiempo.toString() ); } // fin de main } // fin de la clase PruebaTiempo1

La hora universal inicial es: 00:00:00 La hora estandar inicial es: 12:00:00 AM La hora universal despues de establecerTiempo es: 13:27:06 La hora estandar despues de establecerTiempo es: 1:27:06 PM Despues de intentar ajustes invalidos: Hora universal: 00:00:00 Hora estandar: 12:00:00 AM

Figura 8.2 | Objeto Tiempo1 utilizado en una aplicación. La línea 19 invoca al método establecerTiempo del objeto tiempo para modificar la hora. Después las líneas 20 a 24 imprimen en pantalla la hora otra vez en ambos formatos, para confirmar que la hora se haya ajustado en forma apropiada. Para ilustrar que el método establecerTiempo mantiene el objeto en un estado consistente, la línea 27 llama al método establecerTiempo con los argumentos de 99 para la hora, el minuto y el segundo. Las líneas 28 a 32 imprimen de nuevo el tiempo en ambos formatos, para confirmar que establecerTiempo haya mantenido el estado consistente del objeto, y después el programa termina. Las últimas dos líneas de la salida de la aplicación muestran que el tiempo se restablece a medianoche (el valor inicial de un objeto Tiempo1) si tratamos de establecer el tiempo con tres valores fuera de rango.

www.elsolucionario.net

330

Capítulo 8

Clases y objetos: un análisis más detallado

Notas acerca de la declaración de la clase Tiempo1 Es necesario considerar diversas cuestiones sobre el diseño de clases, en relación con la clase Tiempo1. Las variables de instancia hora, minuto y segundo se declaran como private. La representación de datos que se utilice dentro de la clase no concierne a los clientes de la misma. Por ejemplo, sería perfectamente razonable que Tiempo1 representara el tiempo internamente como el número de segundos transcurridos a partir de medianoche, o el número de minutos y segundos transcurridos a partir de medianoche. Los clientes podrían usar los mismos métodos public para obtener los mismos resultados, sin tener que preocuparse por lo anterior. (El ejercicio 8.5 le pide que represente la hora en la clase Tiempo1 como el número de segundos transcurridos a partir de medianoche, y que muestre que, en definitiva, no hay cambios visibles para los clientes de la clase).

Observación de ingeniería de software 8.2 Las clases simplifican la programación, ya que el cliente sólo puede utilizar los métodos public expuestos por la clase. Dichos miembros, por lo general, están orientados a los clientes, en vez de estar orientados a la implementación. Los clientes nunca se percatan de (ni se involucran en) la implementación de una clase. Por lo normal se preocupan acerca de lo que hace la clase, pero no cómo lo hace.

Observación de ingeniería de software 8.3 Las interfaces cambian con menos frecuencia que las implementaciones. Cuando cambia una implementación, el código dependiente de esa implementación debe cambiar de manera acorde. El ocultamiento de la implementación reduce la posibilidad de que otras partes del programa se vuelvan dependientes de los detalles de la implementación de la clase.

8.3 Control del acceso a los miembros Los modificadores de acceso public y private controlan el acceso a las variables y los métodos de una clase (en el capítulo 9, presentaremos el modificador de acceso adicional protected). Como dijimos en la sección 8.2, el principal propósito de los métodos public es presentar a los clientes de la clase una vista de los servicios que proporciona (la interfaz pública de la clase). Los clientes de la clase no necesitan preocuparse por la forma en que la clase realiza sus tareas. Por esta razón, las variables y métodos private de una clase (es decir, los detalles de implementación de la clase) no son directamente accesibles para los clientes de la clase. La figura 8.3 demuestra que los miembros de una clase private no son directamente accesibles fuera de la clase. Las líneas 9 a 11 tratan de acceder en forma directa a las variables de instancia private hora, minuto y segundo del objeto tiempo de la clase Tiempo1. Al compilar este programa, el compilador genera mensajes de error que indican que estos miembros private no son accesibles. [Nota: este programa asume que se utiliza la clase Tiempo1 de la figura 8.1].

Error común de programación 8.1 Cuando un método que no es miembro de una clase trata de acceder a un miembro private de esa clase, se produce un error de compilación.

1 2 3 4 5 6 7 8 9 10 11 12 13

// Fig. 8.3: PruebaAccesoMiembros.java // Los miembros private de la clase Tiempo1 no son accesibles. public class PruebaAccesoMiembros { public static void main( String args[] ) { Tiempo1 tiempo = new Tiempo1(); // crea e inicializa un objeto Tiempo1 tiempo.hora = 7; // error: hora tiene acceso privado en Tiempo1 tiempo.minuto = 15; // error: minuto tiene acceso privado en Tiempo1 tiempo.segundo = 30; // error: segundo tiene acceso privado en Tiempo1 } // fin de main } // fin de la clase PruebaAccesoMiembros

Figura 8.3 | Los miembros private de la clase Tiempo1 no son accesibles. (Parte 1 de 2).

www.elsolucionario.net

8.4

Referencias a los miembros del objeto acual mediante this

331

PruebaAccesoMiembros.java:9: hora has private access in Tiempo1 tiempo.hora = 7; // error: hora tiene acceso privado en Tiempo1 ^ PruebaAccesoMiembros.java:10: minuto has private access in Tiempo1 tiempo.minuto = 15; // error: minuto tiene acceso privado en Tiempo1 ^ PruebaAccesoMiembros.java:11: segundo has private access in Tiempo1 tiempo.segundo = 30; // error: segundo tiene acceso privado en Tiempo1 ^ 3 errors

Figura 8.3 | Los miembros private de la clase Tiempo1 no son accesibles. (Parte 2 de 2).

8.4 Referencias a los miembros del objeto actual mediante this

Cada objeto puede acceder a una referencia a sí mismo mediante la palabra clave this (también conocida como referencia this). Cuando se hace una llamada a un método no static para un objeto específico, el cuerpo del método utiliza en forma implícita la palabra clave this para hacer referencia a las variables de instancia y los demás métodos del objeto. Como verá en la figura 8.4, puede utilizar también la palabra clave this explícitamente en el cuerpo de un método no static. La sección 8.5 muestra otro uso interesante de la palabra clave this. La sección 8.11 explica por qué no puede usarse la palabra clave this en un método static. Ahora demostraremos el uso implícito y explícito de la referencia this para permitir al método main de la clase PruebaThis que muestre en pantalla los datos private de un objeto de la clase TiempoSimple (figura 8.4). Observe que este ejemplo es el primero en el que declaramos dos clases en un archivo; la clase PruebaThis se declara en las líneas 4 a 11 y la clase TiempoSimple se declara en las líneas 14 a 47. Hicimos esto para demostrar que, al compilar un archivo .java que contiene más de una clase, el compilador produce un archivo de clase separado con la extensión .class para cada clase compilada. En este caso se produjeron dos archivos separados: TiempoSimple.class y PruebaThis.class. Cuando un archivo de código fuente (.java) contiene varias declaraciones de clases, el compilador coloca los archivos para esas clases en el mismo directorio. Observe además que sólo la clase PruebaThis se declara public en la figura 8.4. Un archivo de código fuente sólo puede contener una clase public; de lo contrario, se produce un error de compilación.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

// Fig. 8.4: PruebaThis.java // Uso implícito y explícito de this para hacer referencia a los miembros de un objeto. public class PruebaThis { public static void main( String args[] ) { TiempoSimple tiempo = new TiempoSimple( 15, 30, 19 ); System.out.println( tiempo.crearString() ); } // fin de main } // fin de la clase PruebaThis // la clase TiempoSimple demuestra la referencia "this" class TiempoSimple { private int hora; // 0-23 private int minuto; // 0-59 private int segundo; // 0-59 // si el constructor utiliza nombres de parámetros idénticos a

Figura 8.4 | Uso implícito y explícito de this para hacer referencia a los miembros de un objeto. (Parte 1 de 2).

www.elsolucionario.net

332

21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

Capítulo 8

Clases y objetos: un análisis más detallado

// los nombres de las variables de instancia, se requiere la // referencia “this” para diferenciar unos nombres de otros public TiempoSimple( int hora, int minuto, int segundo ) { this.hora = hora; // establece la hora del objeto "this" this.minuto = minuto; // establece el minuto del objeto "this" this.segundo = segundo; // establece el segundo del objeto "this" } // fin del constructor de TiempoSimple // usa la referencia "this" explícita e implícita para llamar aStringUniversal public String crearString() { return String.format( "%24s: %s\n%24s: %s", "this.aStringUniversal()", this.aStringUniversal(), "aStringUniversal()", aStringUniversal() ); } // fin del método crearString // convierte a String en formato de hora universal (HH:MM:SS) public String aStringUniversal() { // "this" no se requiere aquí para acceder a las variables de instancia, // ya que el método no tiene variables locales con los mismos // nombres que las variables de instancia return String.format( "%02d:%02d:%02d", this.hora, this.minuto, this.segundo ); } // fin del método aStringUniversal } // fin de la clase TiempoSimple

this.aStringUniversal(): 15:30:19 aStringUniversal(): 15:30:19

Figura 8.4 | Uso implícito y explícito de this para hacer referencia a los miembros de un objeto. (Parte 2 de 2).

La clase TiempoSimple (líneas 14 a 47) declara tres variables de instancia private: hora, minuto y segundo (líneas 16 a 18). El constructor (líneas 23 a 28) recibe tres argumentos int para inicializar un objeto TiempoSimple. Observe que para el constructor (línea 23) utilizamos nombres de parámetros idénticos a los nombres de las variables de instancia de la clase (líneas 16 a 18). No recomendamos esta práctica, pero lo hicimos aquí para ocultar las variables de instancia correspondientes y así poder ilustrar el uso explícito de la referencia this. Si un método contiene una variable local con el mismo nombre que el de un campo, hará referencia a la variable local en vez del campo. En este caso, la variable local oculta el campo en el alcance del método. No obstante, el método puede utilizar la referencia this para hacer referencia al campo oculto de manera explícita, como se muestra en las líneas 25 a 27 para las variables de instancia ocultas de TiempoSimple. El método crearString (líneas 31 a 36) devuelve un objeto String creado por una instrucción que utiliza la referencia this en forma explícita e implícita. La línea 34 utiliza la referencia this en forma explícita para llamar al método aStringUniversal. La línea 35 usa la referencia this en forma implícita para llamar al mismo método. Observe que ambas líneas realizan la misma tarea. Por lo general, los programadores no utilizan la referencia this en forma explícita para hacer referencia a otros métodos en el objeto actual. Además, observe que la línea 45 en el método aStringUniversal utiliza en forma explícita la referencia this para acceder a cada variable de instancia. Esto no es necesario aquí, ya que el método no tiene variables locales que oculten las variables de instancia de la clase.

Error común de programación 8.2 A menudo se produce un error lógico cuando un método contiene un parámetro o variable local con el mismo nombre que un campo de la clase. En tal caso, use la referencia this si desea acceder al campo de la clase; de no ser así, se hará referencia al parámetro o variable local del método.

www.elsolucionario.net

8.5

Ejemplo práctico de la clase tiempo: constructores sobrecargados

333

Tip para prevenir errores 8.1 Evite los nombres de los parámetros o variables locales que tengan conflicto con los nombres de los campos. Esto ayuda a evitar errores sutiles, difíciles de localizar.

La clase de la aplicación PruebaThis (líneas 4 a 11) demuestra el uso de la clase TiempoSimple. La línea 8 crea una instancia de la clase TiempoSimple e invoca a su constructor. La línea 9 invoca al método crearString del objeto y después muestra los resultados en pantalla.

Tip de rendimiento 8.1 Para conservar la memoria, Java mantiene sólo una copia de cada método por clase; todos los objetos de la clase invocan a este método. Por otro lado, cada objeto tiene su propia copia de las variables de instancia de la clase (es decir, las variables no static). Cada método de la clase utiliza en forma implícita la referencia this para determinar el objeto específico de la clase que se manipulará.

8.5 Ejemplo práctico de la clase Tiempo: constructores sobrecargados Como sabe, puede declarar su propio constructor para especificar cómo deben inicializarse los objetos de una clase. A continuación demostraremos una clase con varios constructores sobrecargados, que permiten a los objetos de esa clase inicializarse de distintas formas. Para sobrecargar los constructores, sólo hay que proporcionar varias declaraciones del constructor con distintas firmas. En la sección 6.12 vimos que el compilador diferencia las firmas en base al número, tipos y orden de los parámetros en cada firma.

La clase Tiempo2 con constructores sobrecargados El constructor predeterminado de la clase Tiempo1 (figura 8.1) inicializó hora, minuto y segundo con sus valores predeterminados de 0 (medianoche en formato de hora universal). El constructor predeterminado no permite que los clientes de la clase inicialicen la hora con valores específicos distintos de cero. La clase Tiempo2 (figura 8.5) contiene cinco constructores sobrecargados que proporcionan formas convenientes para inicializar los objetos de la nueva clase Tiempo2. Cada constructor inicializa el objeto para que empiece en un estado consistente. En este programa, cuatro de los constructores invocan un quinto constructor, el cual a su vez llama al método establecerTiempo para asegurar que el valor suministrado para hora se encuentre en el rango de 0 a 23, y que los valores para minuto y segundo se encuentren cada uno en el rango de 0 a 59. Si un valor está fuera de rango, se establece a 0 mediante establecerTiempo (una vez más se asegura que cada variable de instancia permanezca en un estado consistente). Para invocar el constructor apropiado, el compilador relaciona el número, los tipos y el orden de los argumentos especificados en la llamada al constructor con el número, los tipos y el orden de los tipos de los parámetros especificados en la declaración de cada constructor. Observe que la clase Tiempo2 también proporciona métodos establecer y obtener para cada variable de instancia.

1 2 3 4 5 6 7 8 9 10 11 12 13

// Fig. 8.5: Tiempo2.java // Declaración de la clase Tiempo2 con constructores sobrecargados. public class Tiempo2 { private int hora; // 0 - 23 private int minuto; // 0 - 59 private int segundo; // 0 - 59 // Constructor de Tiempo2 sin argumentos: inicializa cada variable de instancia // a cero; asegura que los objetos Tiempo2 empiecen en un estado consistente public Tiempo2() {

Figura 8.5 | La clase Tiempo2 con constructores sobrecargados. (Parte 1 de 3).

www.elsolucionario.net

334

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70

Capítulo 8

Clases y objetos: un análisis más detallado

this( 0, 0, 0 ); // invoca al constructor de Tiempo2 con tres argumentos } // fin del constructor de Tiempo2 sin argumentos // Constructor de Tiempo2: se suministra hora, minuto y segundo con valor predeterminado de 0 public Tiempo2( int h ) { this( h, 0, 0 ); // invoca al constructor de Tiempo2 con tres argumentos } // fin del constructor de Tiempo2 con un argumento // Constructor de Tiempo2: se suministran hora y minuto, segundo con valor predeterminado de 0 public Tiempo2( int h, int m ) { this( h, m, 0 ); // invoca al constructor de Tiempo2 con tres argumentos } // fin del constructor de Tiempo2 con dos argumentos // Constructor de Tiempo2: public Tiempo2( int h, int { establecerTiempo( h, m, } // fin del constructor de

se suministran hora, minuto y segundo m, int s ) s ); // invoca a establecerTiempo para validar el tiempo Tiempo2 con tres argumentos

// Constructor de Tiempo2: se suministra otro objeto Tiempo2 public Tiempo2( Tiempo2 tiempo ) { // invoca al constructor de Tiempo2 con tres argumentos this( tiempo.obtenerHora(), tiempo.obtenerMinuto(), tiempo.obtenerSegundo() ); } // fin del constructor de Tiempo2 con un objeto Tiempo2 como argumento // Métodos "establecer" // establece un nuevo valor de tiempo usando la hora universal; asegura que // los datos sean consistentes, estableciendo los valores inválidos en cero public void establecerTiempo( int h, int m, int s ) { establecerHora( h ); // establece la hora establecerMinuto( m ); // establece el minuto establecerSegundo( s ); // establece el segundo } // fin del método establecerTiempo // valida y public void { hora = ( } // fin del

establece la hora establecerHora( int h )

// valida y public void { minuto = } // fin del

establece el minuto establecerMinuto( int m )

( h >= 0 && h < 24 ) ? h : 0 ); método establecerHora

( ( m >= 0 && m < 60 ) ? m : 0 ); método establecerMinuto

// valida y establece el segundo public void establecerSegundo( int s ) { segundo = ( ( s >= 0 && s < 60 ) ? s : 0 ); } // fin del método establecerSegundo // Métodos "obtener"

Figura 8.5 | La clase Tiempo2 con constructores sobrecargados. (Parte 2 de 3).

www.elsolucionario.net

8.5

71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103

Ejemplo práctico de la clase tiempo: constructores sobrecargados

335

// obtiene el valor de la hora public int obtenerHora() { return hora; } // fin del método obtenerHora // obtiene el valor del minuto public int obtenerMinuto() { return minuto; } // fin del método obtenerMinuto // obtiene el valor del segundo public int obtenerSegundo() { return segundo; } // fin del método obtenerSegundo // convierte a String en formato de hora universal (HH:MM:SS) public String aStringUniversal() { return String.format( "%02d:%02d:%02d", obtenerHora(), obtenerMinuto(), obtenerSegundo() ); } // fin del método aStringUniversal // convierte a String en formato de hora estándar (H:MM:SS AM o PM) public String toString() { return String.format( "%d:%02d:%02d %s", ( (obtenerHora() == 0 || obtenerHora() == 12) ? 12 : obtenerHora() % 12 ), obtenerMinuto(), obtenerSegundo(), ( obtenerHora() < 12 ? "AM" : "PM" ) ); } // fin del método toString } // fin de la clase Tiempo2

Figura 8.5 | La clase Tiempo2 con constructores sobrecargados. (Parte 3 de 3).

Constructores de la clase Tiempo2 Las líneas 12 a 15 declaran un constructor sin argumentos; es decir, un constructor que se invoca sin argumentos; simplemente inicializa el objeto como se especifica en el cuerpo del constructor. En el cuerpo, presentamos un uso de la referencia this que se permite sólo como la primera instrucción en el cuerpo de un constructor. La línea 14 utiliza a this en la sintaxis de la llamada al método para invocar al constructor de Tiempo2 que recibe tres argumentos (líneas 30 a 33). El constructor sin argumentos pasa los valores de 0 para hora, minuto y segundo al constructor con tres parámetros. El uso de la referencia this que se muestra aquí es una forma popular de reutilizar el código de inicialización que proporciona otro de los constructores de la clase, en vez de definir código similar en el cuerpo del constructor sin argumentos. Utilizamos esta sintaxis en cuatro de los cinco constructores de Tiempo2 para que la clase sea más fácil de mantener y modificar. Si necesitamos cambiar la forma en que se inicializan los objetos de la clase Tiempo2, sólo hay que modificar el constructor al que necesitan llamar los demás constructores de la clase. Incluso hasta ese constructor podría no requerir de modificación en este ejemplo. Simplemente llama al método establecerTiempo para realizar la verdadera inicialización, por lo que es posible que los cambios que pudiera requerir la clase se localicen en los métodos establecer.

Error común de programación 8.3 Es un error de sintaxis utilizar this en el cuerpo de un constructor para llamar a otro constructor de la misma clase, si esa llamada no es la primera instrucción en el constructor. También es un error de sintaxis cuando un método trata de invocar a un constructor directamente, mediante this.

www.elsolucionario.net

336

Capítulo 8

Clases y objetos: un análisis más detallado

Las líneas 18 a 21 declaran un constructor de Tiempo2 con un solo parámetro int que representa la hora, que se pasa con 0 para minuto y segundo al constructor de las líneas 30 a 33. Las líneas 24 a 27 declaran un constructor de Tiempo2 que recibe dos parámetros int, los cuales representan la hora y el minuto, que se pasan con un 0 para segundo al constructor de las líneas 30 a 33. Al igual que el constructor sin argumentos, cada uno de estos constructores invoca al constructor en las líneas 30 a 33 para minimizar la duplicación de código. Las líneas 30 a 33 declaran el constructor Tiempo2 que recibe tres parámetros int, los cuales representan la hora, el minuto y el segundo. Este constructor llama a establecerTiempo para inicializar las variables de instancia con valores consistentes.

Error común de programación 8.4 Un constructor puede llamar a los métodos de la clase. Tenga en cuenta que tal vez las variables de instancia no se encuentren aún en un estado consistente, ya que el constructor está en el proceso de inicializar el objeto. El uso de variables de instancia antes de inicializarlas en forma apropiada es un error lógico.

Las líneas 36 a 40 declaran un constructor de Tiempo2 que recibe una referencia Tiempo2 a otro objeto En este caso, los valores del argumento Tiempo2 se pasan al constructor de tres argumentos en las líneas 30 a 33 para inicializar hora, minuto y segundo. Observe que la línea 39 podría haber accedido en forma directa a los valores hora, minuto y segundo del argumento tiempo del constructor con las variables de instancia tiempo.hora, tiempo.minuto y tiempo.segundo, aun cuando hora, minuto y segundo se declaran como variables private de la clase Tiempo2. Esto se debe a una relación especial entre los objetos de la misma clase. En un momento veremos por qué es preferible utilizar los métodos obtener. Tiempo2.

Observación de ingeniería de software 8.4 Cuando un objeto de una clase tiene una referencia a otro objeto de la misma clase, el primer objeto puede acceder a todos los datos y métodos del segundo objeto (incluyendo los que sean private).

Observaciones en relación con los métodos Establecer y Obtener, y los constructores de la clase Tiempo2 Observe que los métodos establecer y obtener de Tiempo2 se llaman en el cuerpo de la clase. En especial, el método establecerTiempo llama a los métodos establecerHora, establecerMinuto y establecerSegundo en las líneas 47 a 49, y los métodos aStringUniversal y toString llaman a los métodos obtenerHora, obtenerMinuto y obtenerSegundo en la línea 93 y en las líneas 100 y 101, respectivamente. En cada caso, estos métodos podrían haber accedido a los datos private de la clase en forma directa, sin necesidad de llamar a los métodos establecer y obtener. Sin embargo, considere la acción de cambiar la representación del tiempo, de tres valores int (que requieren 12 bytes de memoria) a un solo valor int que represente el número total de segundos transcurridos a partir de medianoche (que requiere sólo 4 bytes de memoria). Si hacemos ese cambio, sólo tendrían que cambiar los cuerpos de los métodos que acceden directamente a los datos private; en especial, los métodos establecer y obtener individuales para hora, minuto y segundo. No habría necesidad de modificar los cuerpos de los métodos establecerTiempo, aStringUniversal o toString, ya que no acceden directamente a los datos. Si se diseña la clase de esta forma, se reduce la probabilidad de que se produzcan errores de programación al momento de alterar la implementación de la clase. De manera similar, cada constructor de Tiempo2 podría escribirse de forma que incluya una copia de las instrucciones apropiadas de los métodos establecerHora, establecerMinuto y establecerSegundo. Esto sería un poco más eficiente, ya que se eliminan la llamada extra al constructor y la llamada a establecerTiempo. No obstante, duplicar las instrucciones en varios métodos o constructores dificulta más el proceso de modificar la representación de datos interna de la clase. Si hacemos que los constructores de Tiempo2 llamen al constructor con tres argumentos (o que incluso llamen a establecerTiempo directamente), cualquier modificación a la implementación de establecerTiempo sólo tendrá que hacerse una vez.

Observación de ingeniería de software 8.5 Al implementar un método de una clase, use los métodos establecer y obtener de la clase para acceder a sus datos private. Esto simplifica el mantenimiento del código y reduce la probabilidad de errores.

www.elsolucionario.net

8.5

Ejemplo práctico de la clase tiempo: constructores sobrecargados

337

Uso de los constructores sobrecargados de la clase Tiempo2 La clase PruebaTiempo2 (figura 8.6) crea seis objetos Tiempo2 (líneas 8 a 13) para invocar a los constructores sobrecargados de Tiempo2. La línea 8 muestra que para invocar el constructor sin argumentos (líneas 12 a 15 de la figura 8.5) se coloca un conjunto vacío de paréntesis después del nombre de la clase, cuando se asigna un objeto Tiempo2 mediante new. Las líneas 9 a 13 del programa demuestran el paso de argumentos a los demás constructores de Tiempo2. La línea 9 invoca al constructor en las líneas 18 a 21 de la figura 8.5. La línea 10 invoca al constructor en las líneas 24 a 27 de la figura 8.5. Las líneas 11 y 12 invocan al constructor en las líneas 30 a 33 de la figura 8.5. La línea 13 invoca al constructor en las líneas 36 a 40 de la figura 8.5. La aplicación muestra en pantalla la representación String de cada objeto Tiempo2 inicializado, para confirmar que cada uno de ellos se haya inicializado en forma apropiada.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

// Fig. 8.6: PruebaTiempo2.java // Uso de constructores sobrecargados para inicializar objetos Tiempo2. public class PruebaTiempo2 { public static void main( String args[] ) { Tiempo2 t1 = new Tiempo2(); Tiempo2 t2 = new Tiempo2( 2 ); Tiempo2 t3 = new Tiempo2( 21, 34 ); Tiempo2 t4 = new Tiempo2( 12, 25, 42 ); Tiempo2 t5 = new Tiempo2( 27, 74, 99 ); Tiempo2 t6 = new Tiempo2( t4 );

// // // // // //

00:00:00 02:00:00 21:34:00 12:25:42 00:00:00 12:25:42

System.out.println( "Se construyo con:" ); System.out.println( "t1: todos los argumentos predeterminados" ); System.out.printf( " %s\n”, t1.aStringUniversal() ); System.out.printf( " %s\n”, t1.toString() ); System.out.println( "t2: se especifico hora; minuto y segundo predeterminados" ); System.out.printf( " %s\n", t2.aStringUniversal() ); System.out.printf( " %s\n", t2.toString() ); System.out.println( "t3: se especificaron hora y minuto; segundo predeterminado" ); System.out.printf( " %s\n", t3.aStringUniversal() ); System.out.printf( " %s\n", t3.toString() ); System.out.println( "t4: se especificaron hora, minuto y segundo" ); System.out.printf( " %s\n", t4.aStringUniversal() ); System.out.printf( " %s\n", t4.toString() ); System.out.println( "t5: se especificaron todos los valores invalidos" ); System.out.printf( " %s\n", t5.aStringUniversal() ); System.out.printf( " %s\n", t5.toString() ); System.out.println( "t6: se especifico el objeto t4 de Tiempo2" ); System.out.printf( " %s\n", t6.aStringUniversal() ); System.out.printf( " %s\n", t6.toString() ); } // fin de main } // fin de la clase PruebaTiempo2

Se construyo con: t1: todos los argumentos predeterminados 00:00:00 12:00:00 AM

Figura 8.6 | Uso de constructores sobrecargados para inicializar objetos Tiempo2. (Parte 1 de 2).

www.elsolucionario.net

338

Capítulo 8

Clases y objetos: un análisis más detallado

t2: se especifico hora; minuto y segundo predeterminados 02:00:00 2:00:00 AM t3: se especificaron hora y minuto; segundo predeterminado 21:34:00 9:34:00 PM t4: se especificaron hora, minuto y segundo 12:25:42 12:25:42 PM t5: se especificaron todos los valores invalidos 00:00:00 2:00:00 AM t6: se especifico el objeto t4 de Tiempo2 12:25:42 12:25:42 PM

Figura 8.6 | Uso de constructores sobrecargados para inicializar objetos Tiempo2. (Parte 2 de 2).

8.6 Constructores predeterminados y sin argumentos Toda clase debe tener cuando menos un constructor. En la sección 3.7 vimos que si no se proporcionan constructores en la declaración de una clase, el compilador crea un constructor predeterminado que no recibe argumentos cuando se le invoca. El constructor predeterminado inicializa las variables de instancia con los valores iniciales especificados en sus declaraciones, o con sus valores predeterminados (cero para los tipos primitivos numéricos, false para los valores boolean y null para las referencias). En la sección 9.4.1 aprenderá que el constructor predeterminado realiza otra tarea, además de inicializar cada variable de instancia con su valor predeterminado. Si su clase declara constructores, el compilador no creará un constructor predeterminado. En este caso, para especificar la inicialización predeterminada para objetos de su clase, debe declarar un constructor sin argumentos (como en las líneas 12 a 15 de la figura 8.5). Al igual que un constructor predeterminado, un constructor sin argumentos se invoca con paréntesis vacíos. Observe que el constructor sin argumentos de Tiempo2 inicializa en forma explícita un objeto Tiempo2; para ello pasa un 0 a cada parámetro del constructor con tres argumentos. Como 0 es el valor predeterminado para las variables de instancia int, el constructor sin argumentos en este ejemplo podría declararse con un cuerpo vacío. En este caso, cada variable de instancia recibiría su valor predeterminado al momento de llamar al constructor sin argumentos. Si omitimos el constructor sin argumentos, los clientes de esta clase no podrían crear un objeto Tiempo2 con la expresión new Tiempo2().

Error común de programación 8.5 Si una clase tiene constructores, pero ninguno de los constructores public son sin argumentos, y si un programa intenta llamar a un constructor sin argumentos para inicializar un objeto de esa clase, se produce un error de compilación. Se puede llamar a un constructor sin argumentos sólo cuando la clase no tiene constructores (en cuyo caso se llama al constructor predeterminado), o si la clase tiene un constructor public sin argumentos.

Observación de ingeniería de software 8.6 Java permite que otros métodos de la clase, además de sus constructores, tengan el mismo nombre de la clase y especifiquen tipos de valores de retorno. Dichos métodos no son constructores, por lo que no se llaman cuando se crea una instancia de un objeto de la clase. Para determinar cuáles métodos son constructores, Java localiza los métodos que tienen el mismo nombre que la clase y que no especifican un tipo de valor de retorno.

8.7 Observaciones acerca de los métodos Establecer y Obtener Como sabe, los campos private de una clase pueden manipularse solamente mediante métodos de esa clase. Una manipulación típica podría ser el ajuste del saldo bancario de un cliente (por ejemplo, una variable de instancia private de una clase llamada CuentaBancaria) mediante un método llamado calcularInteres. Las clases a menudo proporcionan métodos public para permitir a los clientes de la clase establecer (es decir, asignar valores a) u obtener (es decir, recibir los valores de) variables de instancia private.

www.elsolucionario.net

8.7

Observaciones acerca de los métodos Establecer y Obtener

339

Como ejemplo de nomenclatura, un método para establecer la variable de instancia tasaInteres se llamaría típicamente establecerTasaInteres, y un método para obtener la tasaDeInteres se llamaría típicamente obtenerTasaInteres. Los métodos establecer también se conocen comúnmente como métodos mutadores, porque generalmente cambian un valor. Los métodos obtener también se conocen comúnmente como métodos de acceso o métodos de consulta.

Comparación entre los métodos Establecer y Obtener, y los datos public Parece ser que proporcionar herramientas para establecer y obtener es esencialmente lo mismo que hacer las variables de instancia public. Ésta es una sutileza de Java que hace del lenguaje algo tan deseable para la ingeniería de software. Si una variable de instancia se declara como public, cualquier método que tenga una referencia a un objeto que contenga esta variable de instancia podrá leer o escribir en ella. Si una variable de instancia se declara como private, un método obtener public evidentemente permite a otros métodos el acceso a la variable, pero el método obtener puede controlar la manera en que el cliente puede tener acceso a la variable. Por ejemplo, un método obtener podría controlar el formato de los datos que devuelve y, por ende, proteger el código cliente de la representación actual de los datos. Un método establecer public puede (y debe) escudriñar cuidadosamente los intentos por modificar el valor de la variable, para asegurar que el nuevo valor sea apropiado para ese elemento de datos. Por ejemplo, un intento por establecer el día del mes en una fecha 37 sería rechazado, un intento por establecer el peso de una persona en un valor negativo sería rechazado, y así sucesivamente. Entonces, aunque los métodos establecer y obtener proporcionan acceso a los datos privados, el programador restringe su acceso mediante la implementación de los métodos. Esto ayuda a promover la buena ingeniería de software.

Comprobación de validez en los métodos Establecer Los beneficios de la integridad de los datos no se dan automáticamente sólo porque las variables de instancia se declaren como private; el programador debe proporcionar la comprobación de su validez. Java permite a los programadores diseñar mejores programas de una manera conveniente. Los métodos establecer de una clase pueden devolver valores que indiquen que hubo intentos de asignar datos inválidos a los objetos de la clase. Un cliente de la clase puede probar el valor de retorno de un método establecer para determinar si el intento del cliente por modificar el objeto tuvo éxito, y entonces tomar la acción apropiada.

Observación de ingeniería de software 8.7 Cuando sea necesario, proporcione métodos public para cambiar y obtener los valores de las variables de instancia private. Esta arquitectura ayuda a ocultar la implementación de una clase a sus clientes, lo cual mejora la capacidad de modificación de un programa.

Observación de ingeniería de software 8.8 Los diseñadores de clases no necesitan proporcionar métodos establecer u obtener para cada campo private. Estas capacidades deben proporcionarse solamente cuando esto tenga sentido.

Métodos predicados Otro uso común de los métodos de acceso es para evaluar si una condición es verdadera o falsa; por lo general, a dichos métodos se les llama métodos predicados. Un ejemplo sería un método estaVacio para una clase contenedora: una clase capaz de contener muchos objetos, como una lista enlazada, una pila o una cola. (En los capítulos 17 y 19 hablaremos con detalle sobre estas estructuras de datos). Un programa podría evaluar el método estaVacio antes de tratar de leer otro elemento de un objeto contenedor. Un programa podría evaluar un método estaLleno antes de tratar de insertar otro elemento en un objeto contenedor.

Uso de métodos Establecer y Obtener para crear una clase que sea más fácil de depurar y mantener Si sólo un método realiza una tarea específica, como establecer la hora en un objeto Tiempo2, es más fácil depurar y mantener esa clase. Si la hora no se establece en forma apropiada, el código que modifica la variable de instancia hora se localiza en el cuerpo de un método: establecerHora. Así, sus esfuerzos de depuración pueden enfocarse en el método establecerHora.

www.elsolucionario.net

340

Capítulo 8

Clases y objetos: un análisis más detallado

8.8 Composición Una clase puede tener referencias a objetos de otras clases como miembros. A dicha capacidad se le conoce como composición y algunas veces como relación “tiene un”. Por ejemplo, un objeto de la clase RelojAlarma necesita saber la hora actual y la hora en la que se supone sonará su alarma, por lo que es razonable incluir dos referencias a objetos Tiempo como miembros del objeto RelojAlarma.

Observación de ingeniería de software 8.9 La composición es una forma de reutilización de software, en donde una clase tiene como miembros referencias a objetos de otras clases.

Nuestro ejemplo de composición contiene tres clases: Fecha (figura 8.7), Empleado (figura 8.8) y PruebaEmpleado (figura 8.9). La clase Fecha (figura 8.7) declara las variables de instancia mes, dia y anio (líneas 6 a 8) para representar una fecha. El constructor recibe tres parámetros int. La línea 14 invoca el método utilitario comprobarMes (líneas 23 a 33) para validar el mes; un valor fuera de rango se establece en 1 para mantener un estado consistente. La línea 15 asume que el valor de anio es correcto y no lo valida. La línea 16 invoca al método utilitario comprobarDia (líneas 36 a 52) para validar el valor de dia con base en el mes y anio actuales. Las líneas 42 y 43 determinan si el día es correcto, con base en el número de días en el mes específico. Si el día no es correcto, las líneas 46 y 47 determinan si el mes es Febrero, el día 29 y el anio un año bisiesto. Si las líneas 42 a 48 no devuelven un valor correcto para dia, la línea 51 devuelve 1 para mantener la Fecha en un estado consistente. Observe que las líneas 18 y 19 en el constructor muestran en pantalla la referencia this como un objeto String. Como this es una referencia al objeto Fecha actual, se hace una llamada implícita al método toString (líneas 55 a 58) para obtener la representación String del objeto.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

// Fig. 8.7: Fecha.java // Declaración de la clase Fecha. public class Fecha { private int mes; private int dia; private int anio;

// 1-12 // 1-31 con base en el mes // cualquier año

// constructor: llama a comprobarMes para confirmar el valor apropiado para el mes; // llama a comprobarDia para confirmar el valor apropiado para el día public Fecha( int elMes, int elDia, int elAnio ) { mes = comprobarMes( elMes ); // valida el mes anio = elAnio; // pudo validar el año dia = comprobarDia( elDia ); // valida el día System.out.printf( "Constructor de objeto Fecha para la fecha %s\n", this ); } // fin del constructor de Fecha // método utilitario para confirmar el valor apropiado del mes private int comprobarMes( int mesPrueba ) { if ( mesPrueba > 0 && mesPrueba 0 && diaPrueba = 0 && m < 60 ) ? m : 0 ); // valida el minuto segundo = ( ( s >= 0 && s < 60 ) ? s : 0 ); // valida el segundo } // fin del método establecerTiempo // convierte a objeto String en formato de hora universal (HH:MM:SS) public String aStringUniversal() { return String.format( "%02d:%02d:%02d", hora, minuto, segundo ); } // fin del método aStringUniversal // convierte a objeto String en formato de hora estándar (H:MM:SS AM or PM) public String toString() { return String.format( "%d:%02d:%02d %s", ( ( hora == 0 || hora == 12 ) ? 12 : hora % 12 ), minuto, segundo, ( hora < 12 ? "AM" : "PM" ) ); } // fin del método toString } // fin de la clase Tiempo1

Figura 8.18 | Empaquetamiento de la clase Tiempo1 para reutilizarla.

package, las declaraciones import y los comentarios pueden aparecer fuera de las llaves de una declaración de clase. Un archivo de código fuente de Java debe tener el siguiente orden:

1. Una declaración package (si la hay). 2. Declaraciones import (si las hay). 3. Declaraciones de clases. Sólo una de las declaraciones de las clases en un archivo específico pueden ser public; las demás se colocan en el paquete, y sólo las pueden utilizar las otras clases en el mismo paquete. Las clases que no son public están en un paquete, para dar soporte a las clases reutilizables. En un esfuerzo por proporcionar nombres únicos para cada paquete, Sun Microsystems especifica una convención para nombrar paquetes, que todos los programadores de Java deben seguir. Cada nombre de paquete debe empezar con un nombre de dominio de Internet en orden inverso. Por ejemplo, nuestro nombre de dominio es deitel.com, por lo que los nombres de nuestros paquetes empiezan con com.deitel. Para el nombre de dominio suescuela.edu, el nombre del paquete debe empezar con edu.suescuela. Una vez que se invierte el nombre del dominio, podemos elegir cualquier otro nombre que deseemos para nuestro paquete. Si usted forma parte de una empresa con muchas divisiones, o de una universidad con muchas escuelas, tal vez sea conveniente que utilice el nombre de su división o escuela como el siguiente nombre en el paquete. Nosotros optamos por usar jhtp7 como el siguiente nombre en nuestro paquete, para indicar que esta clase es del libro en inglés Java How To Program, Séptima edición. El último nombre en nuestro paquete especifica que es para el capítulo 8 (cap08).

www.elsolucionario.net

8.16

Ejemplo práctico de la clase Tiempo: creación de paquetes

357

Paso 3: compilar la clase empaquetada El paso 3 es compilar la clase, de manera que se almacene en el paquete apropiado. Cuando se compila un archivo de Java que contiene una declaración package, el archivo de clase resultante se coloca en el directorio especificado por la declaración. La declaración package en la figura 8.18 indica que la clase Tiempo1 debe colocarse en el siguiente directorio: com deitel jhtp7 cap08

Los nombres de los directorios especifican la ubicación exacta de las clases en el paquete. Al compilar una clase en un paquete, la opción -d de la línea de comandos de javac hace que el compilador javac cree los directorios apropiados, con base en la declaración package de la clase. Esta opción también especifica en dónde se deben almacenar los directorios. Por ejemplo, en una ventana de comandos utilizamos el siguiente comando de compilación javac -d . Tiempo1.java

para especificar que el primer directorio en el nombre de nuestro paquete debe colocarse en el directorio actual. El punto (.) después de -d en el comando anterior representa el directorio actual en los sistemas operativos Windows, UNIX y Linux (y en varios otros también). Después de ejecutar el comando de compilación, el directorio actual contiene un directorio llamado com, el cual contiene uno llamado deitel, que a su vez contiene uno llamado jhtp7, y este último contiene un directorio llamado cap08. En el directorio cap08 podemos encontrar el archivo Tiempo1.class. [Nota: si no utiliza la opción -d, entonces debe copiar o mover el archivo de clase al directorio del paquete apropiado después de compilarlo]. El nombre package forma parte del nombre de clase completamente calificado, por lo que el nombre de la clase Tiempo1 es en realidad com.deitel.jhtp7.cap08.Tiempo1. Puede utilizar este nombre completamente calificado en sus programas, o puede importar la clase y utilizar su nombre simple (el nombre de la clase por sí solo: Tiempo1) en el programa. Si otro paquete contiene también una clase Tiempo1, los nombres de clase completamente calificados pueden utilizarse para diferenciar una clase de otra en el programa, y evitar un conflicto de nombres (también conocido como colisión de nombres).

Paso 4: importar la clase reutilizable Una vez que la clase se compila y se guarda en su paquete, se puede importar en los programas (paso 4). En la aplicación PruebaPaqueteTiempo1 de la figura 8.19, la línea 3 especifica que la clase Tiempo1 debe importarse para usarla en la clase PruebaPaqueteTiempo1. La clase PruebaPaqueteTiempo1 está en el paquete predeterminado, ya que el archivo .java de la clase no contiene una declaración package. Como las dos clases se encuentran en distintos paquetes, se requiere la declaración import en la línea 3, de manera que la clase PruebaPaqueteTiempo1 pueda utilizar la clase Tiempo1.

1 2 3 4 5 6 7 8 9 10 11 12 13

// Fig. 8.19: PruebaPaqueteTiempo1.java // Uso de un objeto Tiempo1 en una aplicación. import com.deitel.jhtp7.cap08.Tiempo1; // importa la clase Tiempo1 public class PruebaPaqueteTiempo1 { public static void main( String args[] ) { // crea e inicializa un objeto Tiempo1 Tiempo1 tiempo = new Tiempo1(); // llama al constructor de Tiempo1 // imprime representaciones String de la hora System.out.print( "La hora universal inicial es: " );

Figura 8.19 | Uso de un objeto Tiempo1 en una aplicación. (Parte 1 de 2).

www.elsolucionario.net

358

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

Capítulo 8

Clases y objetos: un análisis más detallado

System.out.println( tiempo.aStringUniversal() ); System.out.print( "La hora estandar inicial es: " ); System.out.println( tiempo.toString() ); System.out.println(); // imprime una línea en blanco // cambia la hora e imprime la hora actualizada tiempo.establecerTiempo( 13, 27, 6 ); System.out.print( "La hora universal despues de establecerTiempo es: " ); System.out.println( tiempo.aStringUniversal() ); System.out.print( "La hora estandar despues de establecerTiempo es: " ); System.out.println( tiempo.toString() ); System.out.println(); // imprime una línea en blanco // establece la hora con valores inválidos; imprime la hora actualizada tiempo.establecerTiempo( 99, 99, 99 ); System.out.println( "Despues de intentar ajustes invalidos:" ); System.out.print( "Hora universal: " ); System.out.println( tiempo.aStringUniversal() ); System.out.print( "Hora estandar: " ); System.out.println( tiempo.toString() ); } // fin de main } // fin de la clase PruebaPaqueteTiempo1

La hora universal inicial es: 00:00:00 La hora estandar inicial es: 12:00:00 AM La hora universal despues de establecerTiempo es: 13:27:06 La hora estandar despues de establecerTiempo es: 1:27:06 PM Despues de intentar ajustes invalidos: Hora universal: 00:00:00 Hora estandar: 12:00:00 AM

Figura 8.19 | Uso de un objeto Tiempo1 en una aplicación. (Parte 2 de 2).

La línea 3 se conoce como una declaración import de tipo simple; es decir, la declaración import especifica una clase que se va a importar. Cuando su programa utiliza varias clases del mismo paquete, puede importar esas clases con una sola declaración import. Por ejemplo, la declaración import java.util.*; // importa las clases del paquete java.util

usa un asterisco (*) al final de la declaración import para informar al compilador que todas las clases del paquete java.util están disponibles para usarlas en el programa. Esto se conoce como una declaración import tipo sobre demanda. La JVM sólo carga las clases del paquete java.util que se utilizan en el programa. La declaración import anterior nos permite utilizar el nombre simple de cualquier clase del paquete java.util en el programa. A lo largo de este libro, utilizaremos declaraciones import tipo simples, por claridad.

Error común de programación 8.12 Utilizar la declaración import java.*; produce un error de compilación. Se debe especificar el nombre exacto del paquete del que se desea importar clases.

Especificar la ruta de clases durante la compilación Al compilar PruebaPaqueteTiempo1, javac debe localizar el archivo .class para Tiempo1, de forma que se asegure que la clase PruebaPaqueteTiempo1 utilice a la clase Tiempo1 en forma correcta. El compilador utiliza un objeto especial, llamado cargador de clases, para localizar las clases que necesita. El cargador de clases em-

www.elsolucionario.net

8.16

Ejemplo práctico de la clase Tiempo: creación de paquetes

359

pieza buscando las clases estándar de Java que se incluyen con el JDK. Después busca los paquetes opcionales. Java cuenta con un mecanismo de extensión que permite agregar paquetes nuevos (opcionales), para fines de desarrollo y ejecución. [Nota: el mecanismo de extensión está más allá del alcance de este libro. Para obtener más información, visite java.sun.com/javase/6/docs/technotes/guides/extensions/]. Si la clase no se encuentra en las clases estándar de Java o en las clases de extensión, el cargador de clases busca en la ruta de clases, que contiene una lista de ubicaciones en la que se almacenan las clases. La ruta de clases consiste en una lista de directorios o archivos de ficheros, cada uno separado por un separador de directorio: un signo de punto y coma (;) en Windows o un signo de dos puntos (:) en UNIX/Linux/Mac OS X. Los archivos de ficheros son archivos individuales que contienen directorios de otros archivos, generalmente en formato comprimido. Por ejemplo, las clases estándar de Java que usted utiliza en sus programas están contenidas en el archivo de ficheros rt.jar, el cual se instala junto con el JDK. Los archivos de ficheros generalmente terminan con la extensión .jar o .zip. Los directorios y archivos de ficheros que se especifican en la ruta de clases contienen las clases que usted desea poner a disponibilidad del compilador y la máquina virtual de Java. De manera predeterminada, la ruta de clases consiste sólo del directorio actual. Sin embargo, la ruta de clases puede modificarse de la siguiente manera: 1. proporcionando la opción –classpath al compilador javac o 2. estableciendo la variable de entorno CLASSPATH (una variable especial que usted define y el sistema operativo mantiene, de manera que las aplicaciones puedan buscar clases en las ubicaciones especificadas). Para obtener más información sobre la ruta de clases, visite la página java.sun.com/javase/6/docs/technotes/tools/index.html. La sección titulada “General Information” (información general) contiene información acerca de cómo establecer la ruta de clases para UNIX/Linux y Windows.

Error común de programación 8.13 Al especificar una ruta de clases explícita se elimina el directorio actual de la ruta de clases. Esto evita que las clases en el directorio actual (incluyendo los paquetes en ese directorio) se carguen correctamente. Si deben cargarse clases del directorio actual, un punto (.) en la ruta de clases para especificar el directorio actual.

Observación de ingeniería de software 8.16 En general, es una mejor práctica utilizar la opción –classpath del compilador, en vez de usar la variable de entorno CLASSPATH para especificar la ruta de clases para un programa. De esta manera, cada aplicación puede tener su propia ruta de clases.

Tip para prevenir errores 8.3 Al especificar la ruta de clases con la variable de entorno CLASSPATH se pueden producir errores sutiles y difíciles de localizar en los programas que utilicen versiones distintas del mismo paquete.

Para el ejemplo de las figuras 8.18 y 8.19, no especificamos una ruta de clases explícita. Por lo tanto, para localizar las clases en el paquete com.deitel.jhtp7.cap08 de este ejemplo, el cargador de clases busca en el directorio actual el primer nombre en el paquete: com. A continuación, el cargador de clases navega por la estructura de directorios. El directorio com contiene al subdirectorio deitel; éste contiene al subdirectorio jhtp7. Finalmente, el directorio jhtp7 contiene al subdirectorio cap08. En este subdirectorio se encuentra el archivo Tiempo1.class, que se carga mediante el cargador de clases para asegurar que la clase se utilice apropiadamente en nuestro programa.

Especificar la ruta de clases al ejecutar una aplicación Al ejecutar una aplicación, la JVM debe poder localizar las clases que se utilizan en esa aplicación. Al igual que el compilador, el comando java utiliza un cargador de clases que busca primero en las clases estándar y de extensión, y después busca en la ruta de clases (el directorio actual, de manera predeterminada). La ruta de clases para la JVM puede especificarse en forma explícita, utilizando cualquiera de las técnicas descritas para el compilador. Al igual que con el compilador, es mejor especificar a la JVM una ruta de clases individual para cada programa, mediante las opciones de la línea de comandos. Usted puede especificar la ruta de clases en el comando java

www.elsolucionario.net

360

Capítulo 8

Clases y objetos: un análisis más detallado

mediante las opciones de línea de comandos -classpath o -cp, seguidas de una lista de directorios o archivos de ficheros separados por signos de punto y coma (;) en Microsoft Windows, o signos de dos puntos (:) en UNIX/ Linux/Mac OS X. De nuevo, si las clases deben cargarse del directorio actual, asegúrese de incluir un punto (.) en la ruta de clases para especificar el directorio actual.

8.17 Acceso a paquetes Si no se especifica un modificador de acceso (public, protected o private; hablaremos sobre protected en el capítulo 9) para un método o variable al declararse en una clase, se considerará que el método o variable tiene acceso a nivel de paquete. En un programa que consiste de una declaración de clase, esto no tiene un efecto específico. No obstante, si un programa utiliza varias clases del mismo paquete (es decir, un grupo de clases relacionadas), éstas pueden acceder a los miembros con acceso a nivel de paquete de cada una de las otras clases directamente, a través de referencias a objetos de las clases apropiadas. La aplicación de la figura 8.20 demuestra el acceso a los paquetes. La aplicación contiene dos clases en un archivo de código fuente: la clase de aplicación PruebaDatosPaquete (líneas 5 a 21) y la clase DatosPaquete (líneas 24 a 41). Al compilar este programa, el compilador produce dos archivos .class separados: PruebaDatosPaquete.class y DatosPaquete.class. El compilador coloca los dos archivos .class en el mismo directorio, por lo que las clases se consideran como parte del mismo paquete. Como forman parte del mismo paquete, se permite a la clase PruebaDatosPaquete modificar los datos con acceso a nivel de paquete de los objetos DatosPaquete. En la declaración de la clase DatosPaquete, las líneas 26 y 27 declaran las variables de instancia numero y cadena sin modificadores de acceso; por lo tanto, éstas son variables de instancia con acceso a nivel de paquete. El método main de la aplicación PruebaDatosPaquete crea una instancia de la clase DatosPaquete (línea 9) para demostrar la habilidad de modificar las variables de instancia de DatosPaquete directamente (como se muestra en las líneas 15 y 16). Los resultados de la modificación se pueden ver en la ventana de resultados.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

// Fig. 8.20: PruebaDatosPaquete.java // Los miembros con acceso a nivel de paquete de una clase son accesibles // para las demás clases en el mismo paquete. public class PruebaDatosPaquete { public static void main( String args[] ) { DatosPaquete datosPaquete = new DatosPaquete(); // imprime la representación String de datosPaquete System.out.printf( "Despues de instanciar:\n%s\n", datosPaquete ); // modifica los datos con acceso a nivel de paquete en el objeto datosPaquete datosPaquete.numero = 77; datosPaquete.cadena = "Adios"; // imprime la representación String de datosPaquete System.out.printf( "\nDespues de modificar valores:\n%s\n", datosPaquete ); } // fin de main } // fin de la clase PruebaDatosPaquete // clase con variables de instancia con acceso a nivel de paquete class DatosPaquete { int numero; // variable de instancia con acceso a nivel de paquete String cadena; // variable de instancia con acceso a nivel de paquete

Figura 8.20 | Los miembros con acceso a nivel de paquete de una clase son accesibles para las demás clases en el mismo paquete. (Parte 1 de 2).

www.elsolucionario.net

8.18

28 29 30 31 32 33 34 35 36 37 38 39 40 41

(Opcional) Ejemplo práctico de GUI y gráficos: uso de objetos con gráficos

361

// constructor public DatosPaquete() { numero = 0; cadena = "Hola"; } // fin del constructor de DatosPaquete // devuelve la representación String del objeto DatosPaquete public String toString() { return String.format( "numero: %d; cadena: %s", numero, cadena ); } // fin del método toString } // fin de la clase DatosPaquete

Despues de instanciar: numero: 0; cadena: Hola Despues de modificar valores: numero: 77; cadena: Adios

Figura 8.20 | Los miembros con acceso a nivel de paquete de una clase son accesibles para las demás clases en el mismo paquete. (Parte 2 de 2).

8.18 (Opcional) Ejemplo práctico de GUI y gráficos: uso de objetos con gráficos La mayoría de los gráficos que ha visto hasta este punto no varían cada vez que se ejecuta el programa. Sin embargo, el ejercicio 6.2 le pedía que creara un programa para generar figuras y colores al azar. En ese ejercicio, el dibujo cambiaba cada vez que el sistema llamaba a paintComponent para volver a dibujar el panel. Para crear un dibujo más consistente que permanezca sin cambios cada vez que se dibuja, debemos almacenar información acerca de las figuras mostradas, para que podamos reproducirlas en forma idéntica, cada vez que el sistema llame a paintComponent. Para ello, crearemos un conjunto de clases de figuras que almacenan información acerca de cada figura. Haremos a estas clases “inteligentes”, al permitir que los objetos se dibujen a sí mismos si se les proporciona un objeto Graphics. La figura 8.21 declara la clase MiLinea, que tiene todas estas capacidades. La clase MiLinea importa a Color y Graphics (líneas 3 y 4). Las líneas 8 a 11 declaran variables de instancia para las coordenadas necesarias para dibujar una línea, y la línea 12 declara la variable de instancia que almacena el color. El constructor en las líneas 15 a 22 recibe cinco parámetros, uno para cada variable de instancia que inicializa. El método dibujar en las líneas 25 a 29 requiere un objeto Graphics y lo utiliza para dibujar la línea en el color apropiado y en las coordenadas correctas.

1 2 3 4 5 6 7 8 9 10

// Fig. 8.21: MiLinea.java // Declaración de la clase MiLinea. import java.awt.Color; import java.awt.Graphics; public class MiLinea { private int x1; // coordenada x del primer punto final private int y1; // coordenada y del primer punto final private int x2; // coordenada x del segundo punto final

Figura 8.21 | La clase MiLinea representa a una línea. (Parte 1 de 2).

www.elsolucionario.net

362

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

Capítulo 8

Clases y objetos: un análisis más detallado

private int y2; // coordenada y del segundo punto final private Color miColor; // el color de esta figura // constructor con valores de entrada public MiLinea( int x1, int y1, int x2, int { this.x1 = x1; // establece la coordenada this.y1 = y1; // establece la coordenada this.x2 = x2; // establece la coordenada this.y2 = y2; // establece la coordenada miColor = color; // establece el color } // fin del constructor de MiLinea

y2, Color color ) x y x y

del del del del

primer punto final primer punto final segundo punto final segundo punto final

// Dibuja la línea en el color específico public void dibujar( Graphics g ) { g.setColor( miColor ); g.drawLine( x1, y1, x2, y2 ); } // fin del método dibujar } // fin de la clase MiLinea

Figura 8.21 | La clase MiLinea representa a una línea. (Parte 2 de 2). En la figura 8.22, declaramos la clase PanelDibujo, que generará objetos aleatorios de la clase MiLinea. La línea 12 declara un arreglo MiLinea para almacenar las líneas a dibujar. Dentro del constructor (líneas 15 a 37), la línea 17 establece el color de fondo a Color.WHITE. La línea 19 crea el arreglo con una longitud aleatoria entre 5 y 9. El ciclo en las líneas 22 a 36 crea un nuevo objeto MiLinea para cada elemento en el arreglo. Las líneas 25 a 28 generan coordenadas aleatorias para los puntos finales de cada línea, y las líneas 31 y 32 generan un color aleatorio para la línea. La línea 35 crea un nuevo objeto MiLinea con los valores generados al azar, y lo almacena en el arreglo.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

// Fig. 8.22: PanelDibujo.java // Programa que utiliza la clase MiLinea // para dibujar líneas al azar. import java.awt.Color; import java.awt.Graphics; import java.util.Random; import javax.swing.JPanel; public class PanelDibujo extends JPanel { private Random numerosAleatorios = new Random(); private MiLinea lineas[]; // arreglo de lineas // constructor, crea un panel con figuras al azar public PanelDibujo() { setBackground( Color.WHITE ); lineas = new MiLinea[ 5 + numerosAleatorios.nextInt( 5 ) ]; // crea lineas for ( int cuenta = 0; cuenta < lineas.length; cuenta++ ) { // genera coordenadas aleatorias

Figura 8.22 | Creación de objetos MiLinea al azar. (Parte 1 de 2).

www.elsolucionario.net

8.18

25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

int int int int

x1 y1 x2 y2

= = = =

(Opcional) Ejemplo práctico de GUI y gráficos: uso de objetos con gráficos

numerosAleatorios.nextInt( numerosAleatorios.nextInt( numerosAleatorios.nextInt( numerosAleatorios.nextInt(

300 300 300 300

363

); ); ); );

// genera un color aleatorio Color color = new Color( numerosAleatorios.nextInt( 256 ), numerosAleatorios.nextInt( 256 ), numerosAleatorios.nextInt( 256 ) ); // agrega la línea a la lista de líneas a mostrar en pantalla lineas[ cuenta ] = new MiLinea( x1, y1, x2, y2, color ); } // fin de for } // fin del constructor de PanelDibujo // para cada arreglo de figuras, dibuja las figuras individuales public void paintComponent( Graphics g ) { super.paintComponent( g ); // dibuja las líneas for ( MiLinea linea : lineas ) linea.dibujar( g ); } // fin del método paintComponent } // fin de la clase PanelDibujo

Figura 8.22 | Creación de objetos MiLinea al azar. (Parte 2 de 2).

El método paintComponent itera a través de los objetos MiLinea en el arreglo lineas usando una instrucción for mejorada (líneas 45 y 46). Cada iteración llama al método dibujar del objeto MiLinea actual, y le pasa el objeto Graphics para dibujar en el panel. La clase PruebaDibujo en la figura 8.23 establece una nueva ventana para mostrar nuestro dibujo. Como estableceremos las coordenadas para las líneas sólo una vez en el constructor, el dibujo no cambia si se hace una llamada a paintComponent para actualizar el dibujo en la pantalla.

Ejercicio del ejemplo práctico de GUI y gráficos Extienda el programa de las figuras 8.21 a 8.23 para dibujar rectángulos y óvalos al azar. Cree las clases MiRecy MiOvalo. Ambas deben incluir las coordenadas x1, y1, x2, y2, un color y una bandera boolean para determinar si la figura es rellena. Declare un constructor en cada clase con argumentos para inicializar todas las variables de instancia. Para ayudar a dibujar rectángulos y óvalos, cada clase debe proporcionar los métodos obtenerXSupIzq, obtenerYSupIzq, obtenerAnchura y obtenerAltura, que calculen la coordenada x superior izquierda, la coordenada y superior izquierda, la anchura y la altura, respectivamente. La coordenada x superior izquierda es el más pequeño de los dos valores de coordenada x, la coordenada y superior izquierda es el más pequeño de los dos valores de coordenada y, la anchura es el valor absoluto de la diferencia entre los dos valores de coordenada x, y la altura es el valor absoluto de la diferencia entre los dos valores de coordenada y. La clase PanelDibujo, que extiende a JPanel y se encarga de la creación de las figuras, debe declarar tres arreglos, uno para cada tipo de figura. La longitud de cada arreglo debe ser un número aleatorio entre 1 y 5. El constructor de la clase PanelDibujo debe llenar cada uno de los arreglos con figuras de posición, tamaño, color y relleno aleatorios. Además, modifique las tres clases de figuras para incluir lo siguiente: a) Un constructor sin argumentos que establezca todas las coordenadas de la figura a 0, el color de la figura a Color.BLACK y la propiedad de relleno a false (sólo en MiRectangulo y MiOvalo). b) Métodos establecer para las variables de instancia en cada clase. Los métodos para establecer el valor de una coordenada deben verificar que el argumento sea mayor o igual a cero, antes de establecer la coordenada; si no es así, deben establecer la coordenada a cero. El constructor debe llamar a los métodos establecer, en vez de inicializar las variables locales directamente. c) Métodos obtener para las variables de instancia en cada clase. El método dibujar debe hacer referencia a las coordenadas mediante los métodos obtener, en vez de acceder a ellas directamente. 8.1

tangulo

www.elsolucionario.net

364

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

Capítulo 8

Clases y objetos: un análisis más detallado

// Fig. 8.23: PruebaDibujo.java // Aplicación de prueba para mostrar un PanelDibujo en pantalla. import javax.swing.JFrame; public class PruebaDibujo { public static void main( String args[] ) { PanelDibujo panel = new PanelDibujo(); JFrame aplicacion = new JFrame(); aplicacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); aplicacion.add( panel ); aplicacion.setSize( 300, 300 ); aplicacion.setVisible( true ); } // fin de main } // fin de la clase PruebaDibujo

Figura 8.23 | Creación de un objeto JFrame para mostrar PanelDibujo.

8.19 (Opcional) Ejemplo práctico de Ingeniería de Software: inicio de la programación de las clases del sistema ATM En las secciones del Ejemplo práctico de Ingeniería de Software de los capítulos 1 al 7, hablamos de los fundamentos de la orientación a objetos y desarrollamos un diseño orientado a objetos para nuestro sistema ATM. Anteriormente en este capítulo, vimos muchos de los detalles de programación con clases. Ahora empezaremos a implementar nuestro diseño orientado a objetos en Java. Al final de esta sección, le mostraremos cómo convertir los diagramas de clases en código de Java. En la sección final del Ejemplo práctico de Ingeniería de Software (sección 10.9), modificaremos el código para incorporar el concepto orientado a objetos de herencia. En el apéndice M presentamos la implementación completa del código en Java.

Visibilidad Ahora aplicaremos modificadores de acceso a los miembros de nuestras clases. En el capítulo 3 presentamos 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 empezar 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. En el capítulo 3 observamos 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. Sin embargo, 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

www.elsolucionario.net

8.19

Ejemplo práctico de Ingeniería de Software: inicio de la programación de las clases del sistema ATM 365

modelar la visibilidad de los atributos y las operaciones. La visibilidad pública se indica mediante la colocación de un signo más (+) antes de una operación o atributo, mientras que un signo menos (-) indica una visibilidad privada. La figura 8.24 muestra nuestro diagrama de clases actualizado, en el cual se incluyen los marcadores de visibilidad. [Nota: no incluimos parámetros de operación en la figura 8.24; 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 6.22 a 6.25].

Navegabilidad Antes de empezar a implementar nuestro diseño en Java, presentaremos una notación adicional de UML. El diagrama de clases de la figura 8.25 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 (representadas como flechas con puntas delgadas en el diagrama de clases) indican en qué dirección 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 referencias 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 8.25 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. Observe que las asociaciones en un diagrama de clases que tienen flechas 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.

Cuenta

ATM – usuarioAutenticado : Boolean = false

SolicitudSaldo – numeroCuenta : Integer + ejecutar() Retiro

– numeroCuenta : Integer – nip : Integer – saldoDisponible : Double – saldoTotal : Double + validarNIP() : Boolean + obtenerSaldoDisponible() : Double + obtenerSaldoTotal() : Double + abonar() + cargar()

– numeroCuenta : Integer – monto : Double

Pantalla

+ ejecutar() + mostrarMensaje() Deposito Teclado

– numeroCuenta : Integer – monto : Double

+ obtenerEntrada() : Integer

+ ejecutar() BaseDatosBanco

DispensadorEfectivo – cuenta : Integer = 500

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

+ dispensarEfectivo() + haySuficienteEfectivoDisponible() : Boolean RanuraDeposito + seRecibioSobreDeposito() : Boolean

Figura 8.24 | Diagrama de clases con marcadores de visibilidad.

www.elsolucionario.net

366

Capítulo 8

Clases y objetos: un análisis más detallado

1 Teclado

1

1

DispensadorEfectivo

RanuraDeposito

1

Pantalla

1

1

1 1

1

1

1

0..1 Ejecuta

ATM 1

0..1

0..1

Retiro 0..1 0..1

1 Autentica al usuario contra 1 1 BaseDatosBanco

Contiene

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

1 0..*

Cuenta

Figura 8.25 | Diagrama de clases con flechas de navegabilidad.

Al igual que el diagrama de clases de la figura 3.24, el de la figura 8.25 omite las clases SolicitudSaldo 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 3.10 vimos que SolicitudSaldo tiene una asociación con la clase Pantalla. Podemos navegar de la clase SolicitudSaldo 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 8.25, colocaríamos una flecha de navegabilidad en el extremo de la clase Pantalla de esta asociación. Recuerde 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 colocar 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 10.9, una vez que hayamos simplificado la estructura de nuestro sistema, al incorporar el concepto orientado a objetos de la herencia].

Implementación del sistema ATM a partir de su diseño de UML Ahora estamos listos para empezar a implementar el sistema ATM. Primero convertiremos las clases de los diagramas de las figuras 8.24 y 8.25 en código de Java. Este código representará el “esqueleto” del sistema. En el capítulo 10 modificaremos el código para incorporar el concepto orientado a objetos de la herencia. Como ejemplo, empezaremos a desarrollar el código a partir de nuestro diseño de la clase Retiro en la figura 8.24. Utilizaremos esta figura para determinar los atributos y operaciones de la clase. Usaremos el modelo de UML en la figura 8.25 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 receptáculo para recordarnos que la mayoría de las clases necesitarán en definitiva constructores. En el apéndice M, en donde completamos una versión funcional de esta clase, agregaremos todos los argumentos y el código necesa-

www.elsolucionario.net

8.19

Ejemplo práctico de Ingeniería de Software: inicio de la programación de las clases del sistema ATM 367 rios al cuerpo del constructor. Por ejemplo, la clase Retiro produce el código de la figura 8.26. [Nota: 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 8.27. [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 objetos. Por ejemplo, de acuerdo con la figura 8.25, 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 8.28. [Nota: el constructor de la versión funcional completa de esta clase inicializará estas variables de instancia con referencias a objetos reales].

1 2 3 4 5 6 7 8

// La clase Retiro representa una transacción de retiro del ATM public class Retiro { // constructor sin argumentos public Retiro() { } // fin del constructor de Retiro sin argumentos } // fin de la clase Retiro

Figura 8.26 | Código de Java para la clase Retiro, con base en las figuras 8.24 y 8.25.

1 2 3 4 5 6 7 8 9 10 11 12

// La clase Retiro representa una transacción de retiro del ATM public class Retiro { // atributos private int numeroCuenta; // cuenta de la que se van a retirar los fondos private double monto; // monto que se va a retirar de la cuenta // constructor sin argumentos public Retiro() { } // fin del constructor de Retiro sin argumentos } // fin de la clase Retiro

Figura 8.27 | Código de Java para la clase Retiro, con base en las figuras 8.24 y 8.25.

1 2 3 4 5 6 7 8 9 10 11

// La clase Retiro representa una transacción de retiro del ATM public class Retiro { // atributos private int numeroCuenta; // cuenta de la que se retirarán los fondos private double monto; // monto a retirar // referencias a los objetos asociados private Pantalla pantalla; // pantalla del ATM private Teclado teclado; // teclado del ATM private DispensadorEfectivo dispensadorEfectivo; // dispensador de efectivo del ATM

Figura 8.28 | Código de Java para la clase Retiro, con base en las figuras 8.24 y 8.25. (Parte 1 de 2).

www.elsolucionario.net

368

12 13 14 15 16 17 18

Capítulo 8

Clases y objetos: un análisis más detallado

private BaseDatosBanco baseDatosBanco; // base de datos de información de las cuentas // constructor sin argumentos public Retiro() { } // fin del constructor de Retiro sin argumentos } // fin de la clase Retiro

Figura 8.28 | Código de Java para la clase Retiro, con base en las figuras 8.24 y 8.25. (Parte 2 de 2).

4. Use las operaciones que se localizan en el tercer compartimiento de la figura 8.24 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 6.22 a 6.25 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 8.29. [Nota: codificaremos los cuerpos de los métodos cuando implementemos el sistema ATM completo en el apéndice M].

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

// La clase Retiro representa una transacción de retiro del ATM public class Retiro { // atributos private int numeroCuenta; // cuenta de la que se van a retirar los fondos private double monto; // monto a retirar // referencias a los objetos asociados private Pantalla pantalla; // pantalla del ATM private Teclado teclado; // teclado del ATM private DispensadorEfectivo dispensadorEfectivo; // dispensador de efectivo del ATM private BaseDatosBanco baseDatosBanco; // base de datos de información de las cuentas // constructor sin argumentos public Retiro() { } // fin del constructor de Retiro sin argumentos // operaciones public void ejecutar() { } // fin del método ejecutar } // fin de la clase Retiro

Figura 8.29 | Código de Java para la clase Retiro, con base en las figuras 8.24 y 8.25. Esto concluye nuestra discusión sobre los fundamentos de la generación de clases a partir de diagramas de UML.

Ejercicios de autoevaluación del Ejemplo práctico de Ingeniería de Software 8.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. 8.2

En la figura 8.25, 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.

www.elsolucionario.net

8.20

Conclusión

369

c) (a) y (b); la asociación es bidireccional. d) Ninguna de las anteriores. 8.3

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

Respuestas a los ejercicios de autoevaluación del Ejemplo práctico de Ingeniería de Software 8.1

Verdadero. El signo menos (-) indica visibilidad privada.

8.2

b.

8.3 El diseño para la clase Teclado produce el código de la figura 8.30. Recuerde que la clase Teclado no tiene atributos en estos momentos, pero pueden volverse aparentes a medida que continuemos con la implementación. Observe además que, 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 el apéndice M.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

// la clase Teclado representa el teclado de un ATM public class Teclado { // no se han especificado atributos todavía // constructor sin argumentos public Teclado() { } // fin del constructor de Teclado sin argumentos // operaciones public int obtenerEntrada() { } // fin del método obtenerEntrada } // fin de la clase Teclado

Figura 8.30 | Código de Java para la clase Teclado, con base en las figuras 8.24 y 8.25.

8.20 Conclusión En este capítulo presentamos conceptos adicionales de las clases. El ejemplo práctico de la clase Tiempo presentó una declaración de clase completa que consiste de datos private, constructores public sobrecargados para flexibilidad en la inicialización, métodos establecer y obtener para manipular los datos de la clase, y métodos que devuelven representaciones String de un objeto Tiempo en dos formatos distintos. Aprendió también que toda clase puede declarar un método toString que devuelva una representación String de un objeto de la clase, y que este método puede invocarse en forma implícita siempre que aparezca en el código un objeto de una clase, en donde se espera un String. Aprendió que la referencia this se utiliza en forma implícita en los métodos no static de una clase para acceder a las variables de instancia de la clase y a otros métodos no static. Vio usos explícitos de la referencia this para acceder a los miembros de la clase (incluyendo los campos ocultos) y aprendió a utilizar la palabra clave this en un constructor para llamar a otro constructor de la clase. Hablamos sobre las diferencias entre los constructores predeterminados que proporciona el compilador, y los constructores sin argumentos que proporciona el programador. Aprendió que una clase puede tener referencias a los objetos de otras clases como miembros; un concepto conocido como composición. Vio el tipo de clase enum y aprendió a usarlo para crear un conjunto de constantes para usarlas en un programa. Aprendió acerca de la capacidad de recolección de basura de Java y cómo reclama la memoria de los objetos que ya no se utilizan. Explicamos la motivación para los campos static en una clase, y le demostramos cómo declarar y utilizar campos y métodos static en sus propias clases. También aprendió a declarar e inicializar variables final. Aprendió a empaquetar sus propias clases para reutilizarlas, y cómo importar esas clases en una aplicación. Le mostramos cómo crear una biblioteca de clases para reutilizar componentes, y cómo utilizar las clases de la

www.elsolucionario.net

370

Capítulo 8

Clases y objetos: un análisis más detallado

biblioteca en una aplicación. Por último, aprendió que los campos que se declaran sin un modificador de acceso reciben un acceso a nivel de paquete, de manera predeterminada. Vio la relación entre las clases en el mismo paquete, que permite a cada clase en un paquete acceder a los miembros con acceso a nivel de paquete de otras clases en ese mismo paquete. En el siguiente capítulo aprenderá acerca de un aspecto importante de la programación orientada a objetos en Java: la herencia. En ese capítulo verá que todas las clases en Java se relacionan en forma directa o indirecta con la clase llamada Object. También empezará a comprender cómo las relaciones entre las clases le permiten crear aplicaciones más poderosas.

Resumen Sección 8.2 Ejemplo práctico de la clase Tiempo • Toda clase que usted declara representa un nuevo tipo en Java. • Los métodos public de una clase se conocen también como los servicios public de la clase, o su interfaz public. El propósito principal de los estos métodos es presentar a los clientes de la clase una vista de los servicios que ésta proporciona. Los clientes de la clase no se necesitan preocupar por la forma en que ésta realiza sus tareas. Por esta razón, los miembros de clase private no son directamente accesibles para los clientes de la clase. • Un objeto que contiene datos consistentes tiene valores de datos que siempre se mantienen dentro del rango. • Un valor que se pasa a un método para modificar una variable de instancia es un valor correcto, si se encuentra dentro del rango permitido para la variable de instancia. Un valor correcto siempre consistente, pero un valor consistente no es correcto si un método recibe un valor fuera de rango, y lo establece en un valor consistente para mantener el objeto en un estado consistente. • El método static format de la clase String es similar al método System.out.printf, excepto que format devuelve un objeto String con formato, en vez de mostrarlo en una ventana de comandos. • Todos los objetos en Java tienen un método toString, que devuelve una representación String del objeto. El método toString se llama en forma implícita cuando aparece un objeto en el código en donde se requiere un String.

Sección 8.4 Referencias a los miembros del objeto actual mediante this • Un método no static de un objeto utiliza en forma implícita la palabra clave this para hacer referencia a las variables de instancia del objeto, y a los demás métodos. La palabra clave this también se puede utilizar en forma explícita. • El compilador produce un archivo separado con la extensión .class para cada clase compilada. • Si un método contiene una variable local con el mismo nombre que uno de los campos de su clase, la variable local oculta el campo en el alcance del método. El método puede usar la referencia this para hacer referencia al campo oculto en forma explícita.

Sección 8.5 Ejemplo práctico de la clase Tiempo: constructores sobrecargados • Los constructores sobrecargados permiten inicializar los objetos de una clase de varias formas distintas. El compilador diferencia a los constructores sobrecargados en base a sus firmas.

Sección 8.6 Constructores predeterminados y sin argumentos • Toda clase debe tener por lo menos un constructor. Si no se proporciona uno, el compilador crea un constructor predeterminado, que inicializa las variables de instancia con los valores iniciales especificados en sus declaraciones, o con sus valores predeterminados. • Si una clase declara constructores, el compilador no crea un constructor predeterminado. Para especificar la inicialización predeterminada para los objetos de una clase con varios constructores, el programador debe declarar un constructor sin argumentos.

Sección 8.7 Observaciones acerca de los métodos Establecer y Obtener • Los métodos establecer se conocen comúnmente como métodos mutadores, ya que, por lo general, cambian un valor. Los métodos obtener se conocen comúnmente como métodos de acceso o de consulta. Un método predicado evalúa si una condición es verdadera o falsa.

www.elsolucionario.net

Resumen

371

Sección 8.8 Composición • Una clase puede tener referencias a objetos de otras clases como miembros. A dicha capacidad se le conoce como composición, y algunas veces se le denomina relación tiene un.

Sección 8.9 Enumeraciones • Todos los tipos enum son tipos por referencia. Un tipo enum se declara con una declaración enum, que es una lista separada por comas de constantes enum. La declaración puede incluir, de manera opcional, otros componentes de las clases tradicionales, como: constructores, campos y métodos. • Los tipos enum son implícitamente final, ya que declaran constantes que no deben modificarse. • Las constantes enum son implícitamente static. • Cualquier intento por crear un objeto de un tipo enum con el operador new produce un error de compilación. • Las constantes enum se pueden utilizar en cualquier parte en donde pueden usarse constantes, como en las etiquetas case de las instrucciones switch y para controlar las instrucciones for mejoradas. • Cada constante enum en una declaración enum va seguida opcionalmente de argumentos que se pasan al constructor de la enum. • Para cada enum, el compilador genera un método static llamado values, que devuelve un arreglo de las constantes de la enum, en el orden en el que se declararon. • El método static range de EnumSet recibe dos parámetros: la primera constante enum en un rango y la última constante enum en un rango; y devuelve un objeto EnumSet que contiene todas las constantes entre estas dos constantes, inclusive.

Sección 8.10 Recolección de basura y el método finalize • Toda clase en Java tiene los métodos de la clase Object, uno de los cuales es finalize. • La Máquina Virtual de Java (JVM) realiza la recolección automática de basura para reclamar la memoria que ocupan los objetos que ya no se utilizan. Cuando ya no hay más referencias a un objeto, la JVM lo marca para la recolección de basura. La memoria para dicho objeto se puede reclamar cuando la JVM ejecuta su recolector de basura. • El método finalize es invocado por el recolector de basura, justo antes de que reclame la memoria del objeto. El método finalize no recibe parámetros y tiene el tipo de retorno void. • Tal vez el recolector de basura nunca se ejecute antes de que un programa termine. Por lo tanto, no queda claro si se hará una llamada al método finalize (o cuándo se hará).

Sección 8.11 Miembros de clase static • Una variable static representa la información a nivel de clase que se comparte entre todos los objetos de la clase. • Las variables static tienen alcance en toda la clase. Se puede tener acceso a los miembros public static de una clase a través de una referencia a cualquier objeto de la clase, o calificando el nombre del miembro con el nombre de la clase y un punto (.). El acceso a los miembros private static de una clase se obtiene sólo a través de los métodos de la clase. • Los miembros de clase static existen aun cuando no existan objetos de la clase; están disponibles tan pronto como se carga la clase en memoria, en tiempo de ejecución. Para acceder a un miembro private static cuando no existen objetos de la clase, debe proporcionarse un método public static. • El método static gc de la clase System indica que el recolector de basura debe realizar su mejor esfuerzo al tratar de reclamar objetos que sean candidatos para la recolección de basura. • Un método que se declara como static no puede acceder a los miembros de clase que no son static, ya que un método static puede llamarse incluso aunque no se hayan creado instancias de objetos de la clase. • La referencia this no puede utilizarse en un método static.

Sección 8.12 Declaración static import • Una declaración static import permite a los programadores hacer referencia a los miembros static importados, sin tener que utilizar el nombre de la clase y un punto (.). Una declaración static import individual importa un miembro static, y una declaración static import sobre demanda importa a todos los miembros static de una clase.

Sección 8.13 Variables de instancia final • En el contexto de una aplicación, el principio del menor privilegio establece que al código se le debe otorgar sólo el nivel de privilegio y de acceso que necesita para realizar su tarea designada. • La palabra clave final especifica que una variable no puede modificarse; en otras palabras, es constante. Las constantes pueden inicializarse cuando se declaran, o por medio de cada uno de los constructores de una clase. Si una variable final no se inicializa, se produce un error de compilación.

www.elsolucionario.net

372

Capítulo 8

Clases y objetos: un análisis más detallado

Sección 8.14 Reutilización de software • El software se construye a partir de componentes existentes, bien definidos, cuidadosamente probados, bien documentados, portables y con amplia disponibilidad. La reutilización de software agiliza el desarrollo de software poderoso, de alta calidad. El desarrollo rápido de aplicaciones (RAD) es de gran interés hoy en día. • Ahora, los programadores de Java tienen miles de clases en la API a su disposición, para ayudarse a implementar programas en Java. Las clases de la API de Java permiten a los programadores de Java llevar nuevas aplicaciones al mercado con más rapidez, mediante el uso de componentes pre-existentes y probados.

Sección 8.15 Abstracción de datos y encapsulamiento • El cliente de una clase se preocupa acerca de la funcionalidad que ésta ofrece, pero no acerca de cómo se implementa esta funcionalidad. A esto se le conoce como abstracción de datos. Aunque los programadores pueden conocer los detalles de la implementación de una clase, no deben escribir código que dependa de estos detalles. Esto nos permite reemplazar una clase con otra versión, sin afectar el resto del sistema. • Un tipo de datos abstracto (ADT) consiste en una representación de datos y las operaciones que pueden realizarse con esos datos.

Sección 8.16 Ejemplo práctico de la clase Tiempo: creación de paquetes • Cada clase en la API de Java pertenece a un paquete que contiene un grupo de clases relacionadas. Los paquetes ayudan a administrar la complejidad de los componentes de una aplicación, y facilitan la reutilización de software. • Los paquetes proporcionan una convención para los nombres de clases únicos, que ayuda a evitar los conflictos de nombres de clases. • Antes de poder importar una clase en varias aplicaciones, ésta debe colocarse en un paquete. Sólo puede haber una declaración package en cada archivo de código fuente de Java, y debe ir antes de todas las demás declaraciones e instrucciones en el archivo. • Cada nombre de paquete debe empezar con el nombre de dominio de Internet del programador, en orden inverso. Una vez que se invierte el nombre de dominio, podemos elegir cualquier otro nombre que deseemos para nuestro paquete. • Al compilar una clase en un paquete, la opción -d de línea de comandos de javac especifica en dónde se debe almacenar el paquete, y hace que el compilador cree los directorios del paquete, en caso de que no existan. • El nombre del paquete forma parte del nombre completamente calificado de una clase. Esto ayuda a evitar los conflictos de nombres. • Una declaración import de tipo simple especifica una clase a importar. Una declaración import de tipo por demanda sólo importa las clases que el programa utilice de un paquete específico. • El compilador utiliza un cargador de clases para localizar las clases que necesita en la ruta de clases. La ruta de clases consiste en una lista de directorios o archivos de ficheros, cada uno separado por un separador de directorio. • La ruta de clases para el compilador y la JVM se puede especificar proporcionando la opción –classpath al comando javac o java, o estableciendo la variable de entorno CLASSPATH. La ruta de clases para la JVM también se puede especificar mediante la opción -cp de línea de comandos. Si las clases deben cargarse del directorio actual, incluya un punto (.) en la ruta de clases.

Sección 8.17 Acceso a paquetes • Si no se especifica un modificador de acceso para un método o variable al momento de su declaración en una clase, se considera que el método o variable tiene acceso a nivel de paquete.

Terminología -classpath, argumento de línea de comandos para javac -d, argumento de línea de comandos para javac

abstracción de datos acceso a nivel de paquete alcance de clase archivo de ficheros atributo biblioteca de clases cargador de clases clase contenedora

CLASSPATH,

variable de entorno colisión de nombres comportamiento composición comprobación de validez conflicto de nombres constructor predeterminado constructor sin argumentos constructores sobrecargados declaración import de tipo por demanda declaración import de tipo simple

www.elsolucionario.net

Ejercicios de autoevaluación declaración static import simple desarrollo rápido de aplicaciones (RAD) desbordamiento aritmético enum, constante enum, palabra clave EnumSet, clase finalize, método format, método de la clase String fuga de memoria fuga de recursos gc, método de la clase System información a nivel de clase lenguaje extensible marcar un objeto para la recolección de basura mecanismo de extensiones método de consulta método mutador método predicado modificador de acceso nombre simple de una clase, campo o método package, declaración paquete opcional pila primero en entrar, primero en salir (PEPS)

373

principio de menor privilegio private, modificador de acceso protected, modificador de acceso public, interfaz public, modificador de acceso public, servicio range, método de EnumSet recolector de basura representación de datos ruta de clases separador de directorio servicio de una clase static, campo (variable de clase) static, declaración import static, declaración import por demanda tareas de preparación para la terminación this, palabra clave tiene un, relación tipo de datos abstracto (ADT) último en entrar, primero en salir (UEPS) values, método de una enum variable constante variable de clase variable que no se puede modificar

Ejercicio de autoevaluación 8.1

Complete los siguientes enunciados: a) Al compilar una clase en un paquete, la opción __________ de línea de comandos de javac especifica en dónde se debe almacenar el paquete, y hace que el compilador cree los directorios, en caso de que no existan. b) El método static __________ de la clase String es similar al método System.out.printf, pero devuelve un objeto String con formato en vez de mostrar un objeto String en una ventana de comandos. c) Si un método contiene una variable local con el mismo nombre que uno de los campos de su clase, la variable local __________ al campo en el alcance de ese método. d) El recolector de basura llama al método __________ antes de reclamar la memoria de un objeto. e) Una declaración __________ especifica una clase a importar. f ) Si una clase declara constructores, el compilador no creará un(a) __________. g) El método __________ de un objeto se llama en forma implícita cuando aparece un objeto en el código, en donde se necesita un String. h) A los métodos establecer se les llama comúnmente __________ o __________. i) Un método __________ evalúa si una condición es verdadera o falsa. j) Para cada enum, el compilador genera un método static llamado __________, que devuelve un arreglo de las constantes de la enum en el orden en el que se declararon. k) A la composición se le conoce algunas veces como relación __________. l) Una declaración __________ contiene una lista separada por comas de constantes. m) Una variable __________ representa información a nivel de clase, que comparten todos los objetos de la clase. n) Una declaración __________ importa un miembro static. o) El __________ establece que al código se le debe otorgar sólo el nivel de privilegio y de acceso que necesita para realizar su tarea designada. p) La palabra clave __________ especifica que una variable no se puede modificar. q) Un(a) __________ consiste en una representación de datos y las operaciones que pueden realizarse sobre esos datos.

www.elsolucionario.net

374

Capítulo 8

Clases y objetos: un análisis más detallado

r) Sólo puede haber un(a) __________ en un archivo de código fuente de Java, y debe ir antes de todas las demás declaraciones e instrucciones en el archivo. s) Un(a) declaración __________ sólo importa las clases que utiliza el programa de un paquete específico. t) El compilador utiliza un(a) __________ para localizar las clases que necesita en la ruta de clases. u) La ruta de clases para el compilador y la JVM se puede especificar mediante la opción __________ para el comando javac o java, o estableciendo la variable de entorno __________. v) A los métodos establecer se les conoce comúnmente como __________, ya que, por lo general, modifican un valor. w) Un(a) __________ importa a todos los miembros static de una clase. x) Los métodos public de una clase se conocen también como los __________ o __________ de la clase. y) El método static __________ de la clase System indica que el recolector de basura debe realizar su mejor esfuerzo para tratar de reclamar los objetos que sean candidatos para la recolección de basura. z) Un objeto que contiene __________ tiene valores de datos que siempre se mantienen dentro del rango.

Respuestas a los ejercicios de autoevaluación 8.1 a) –d. b) format. c) oculta. d) finalize. e) import de tipo simple. f ) constructor predeterminado. g) toString. h) métodos de acceso, métodos de consulta. i) predicado. j) values. k) tiene un. l) enum. m) static. n) static import de tipo simple. o) principio de menor privilegio. p) final. q) tipo de datos abstracto (ADT). r) declaración package. s) import tipo sobre demanda. t) cargador de clases. u) -classpath, CLASSPATH. v) métodos mutadores. w) declaración static import sobre demanda. x) servicios public, interfaz public. y) gc. z) datos consistentes.

Ejercicios 8.2 Explique la noción del acceso a nivel de paquete en Java. Explique los aspectos negativos del acceso a nivel de paquete. 8.3

¿Qué ocurre cuando un tipo de valor de retorno, incluso void, se especifica para un constructor?

8.4 (Clase Rectangulo) Cree una clase llamada Rectangulo. La clase debe tener los atributos longitud y anchura, cada uno con un valor predeterminado de 1. Debe tener métodos para calcular el perimetro y el area del rectángulo. Debe tener métodos establecer y obtener para longitud y anchura. Los métodos establecer deben verificar que longitud y anchura sean números de punto flotante mayores de 0.0, y menores de 20.0. Escriba un programa para probar la clase Rectangulo. (Modificación de la representación de datos interna de una clase) Sería perfectamente razonable para la clase Tiemde la figura 8.5 representar la hora internamente como el número de segundos transcurridos desde medianoche, en vez de usar los tres valores enteros hora, minuto y segundo. Los clientes podrían usar los mismos métodos public y obtener los mismos resultados. Modifique la clase Tiempo2 de la figura 8.5 para implementar un objeto Tiempo2 como el número de segundos transcurridos desde medianoche, y mostrar que no hay cambios visibles para los clientes de la clase.

8.5

po2

(Clase cuenta de ahorros) Cree una clase llamada CuentaDeAhorros. Use una variable static llamada tasapara almacenar la tasa de interés anual para todos los cuentahabientes. Cada objeto de la clase debe contener una variable de instancia private llamada saldoAhorros, que indique la cantidad que el ahorrador tiene actualmente en depósito. Proporcione el método calcularInteresMensual para calcular el interés mensual, multiplicando el saldoAhorros por la tasaInteresAnual dividida entre 12; este interés debe sumarse al saldoAhorros. Proporcione un método static llamado modificarTasaInteres para establecer la tasaInteresAnual en un nuevo valor. Escriba un programa para probar la clase CuentaDeAhorros. Cree dos instancias de objetos CuentaDeAhorros, ahorrador1 y ahorrador2, con saldos de $2000.00 y $3000.00, respectivamente. Establezca la tasaInteresAnual en 4%, después calcule el interés mensual e imprima los nuevos saldos para ambos ahorradores. Luego establezca la tasaInteresAnual en 5%, calcule el interés del siguiente mes e imprima los nuevos saldos para ambos ahorradores. 8.6

InteresAnual

8.7 (Mejora a la clase Tiempo2) Modifique la clase Tiempo2 de la figura 8.5 para incluir un método tictac, que incremente el tiempo almacenado en un objeto Tiempo2 por un segundo. Proporcione el método incrementarMinuto para incrementar el minuto, y el método incrementarHora para incrementar la hora. El objeto Tiempo2 debe permanecer siempre en un estado consistente. Escriba un programa para probar los métodos tictac, incrementarMinuto y incrementarHora, para asegurarse que funcionen correctamente. Asegúrese de probar los siguientes casos:

www.elsolucionario.net

Ejercicios

375

a) Incrementar el minuto, de manera que cambie al siguiente minuto. b) Incrementar la hora, de manera que cambie a la siguiente hora. c) Incrementar el tiempo de manera que cambie al siguiente día (por ejemplo, de 11:59:59 PM a 12:00:00 AM). 8.8 (Mejora a la clase Fecha) Modifique la clase Fecha de la figura 8.7 para realizar la comprobación de errores en los valores inicializadores para las variables de instancia mes, dia y anio (la versión actual sólo valida el mes y el día). Proporcione un método llamado siguienteDia para incrementar el día en uno. El objeto Fecha siempre deberá permanecer en un estado consistente. Escriba un programa que evalúe el método siguienteDia en un ciclo que imprima la fecha durante cada iteración del ciclo, para mostrar que el método siguienteDia funciona correctamente. Pruebe los siguientes casos: a) Incrementar la fecha de manera que cambie al siguiente mes. b) Incrementar la fecha de manera que cambie al siguiente año. 8.9 (Devolver indicadores de errores de los métodos) Modifique los métodos establecer en la clase Tiempo2 de la figura 8.5 para devolver valores de error apropiados si se hace un intento por establecer una de las variables de instancia hora, minuto o segundo de un objeto de la clase Tiempo, en un valor inválido. [Sugerencia: use tipos de valores de retorno boolean en cada método]. Escriba un programa que pruebe estos nuevos métodos establecer y que imprima mensajes de error cuando se reciban valores incorrectos. 8.10 static

Vuelva a escribir la figura 8.14, de manera que utilice una declaración de la clase Math que se utilice en el ejemplo.

import

separada para cada miembro

8.11 Escriba un tipo enum llamado LuzSemaforo, cuyas constantes (ROJO, VERDE, AMARILLO) reciban un parámetro: la duración de la luz. Escriba un programa para probar la enum LuzSemaforo, de manera que muestre las constantes de la enum y sus duraciones. 8.12 (Números complejos) Cree una clase llamada Complejo para realizar operaciones aritméticas con números complejos. Estos números tienen la forma parteReal + parteImaginaria * i en donde i es

√-1 Escriba un programa para probar su clase. Use variables de punto flotante para representar los datos private de la clase. Proporcione un constructor que permita que un objeto de esta clase se inicialice al declararse. Proporcione un constructor sin argumentos con valores predeterminados, en caso de que no se proporcionen inicializadores. Proporcione métodos public que realicen las siguientes operaciones: a) Sumar dos números Complejo: las partes reales se suman entre sí y las partes imaginarias también. b) Restar dos números Complejo: la parte real del operando derecho se resta de la parte real del operando izquierdo, y la parte imaginaria del operando derecho se resta de la parte imaginaria del operando izquierdo. c) Imprimir números Complejo en la forma (a, b), en donde a es la parte real y b es la imaginaria. 8.13 (Clase Fecha y Tiempo) Cree una clase llamada FechaYTiempo, que combine la clase Tiempo2 modificada del ejercicio 8.7 y la clase Fecha modificada del ejercicio 8.8. Modifique el método incrementarHora para llamar al método siguienteDia si el tiempo se incrementa hasta el siguiente día. Modifique los métodos aStringEstandar y aStringUniversal para imprimir la fecha, junto con la hora. Escriba un programa para evaluar la nueva clase FechaYTiempo. En específico, pruebe incrementando la hora para que cambie al siguiente día. 8.14 (Clase Rectangulo mejorada) Cree una clase Rectangulo más sofisticada que la que creó en el ejercicio 8.4. Esta clase debe guardar solamente las coordenadas Cartesianas de las cuatro esquinas del rectángulo. El constructor debe llamar a un método establecer que acepte cuatro conjuntos de coordenadas y verifique que cada una de éstas se encuentre en el primer cuadrante, en donde ninguna coordenada x o y debe ser mayor de 20.0. El método establecer debe también verificar que las coordenadas proporcionadas especifiquen un rectángulo. Proporcione métodos para calcular la longitud, anchura, perimetro y area. La longitud será la mayor de las dos dimensiones. Incluya un método predicado llamado esCuadrado, el cual determine si el rectángulo es un cuadrado. Escriba un programa para probar la clase Rectangulo. 8.15 (Conjunto de enteros) Cree la clase ConjuntoEnteros. Cada objeto ConjuntoEnteros puede almacenar enteros en el rango de 0 a 100. El conjunto se representa mediante un arreglo de valores boolean. El elemento del arreglo a[i] es true si el entero i se encuentra en el conjunto. El elemento del arreglo a[j] es false si el entero j no se encuentra

www.elsolucionario.net

376

Capítulo 8

Clases y objetos: un análisis más detallado

dentro del conjunto. El constructor sin argumentos inicializa el arreglo de Java con el “conjunto vacío” (es decir, un conjunto cuya representación de arreglo contiene sólo valores false). Proporcione los siguientes métodos: el método union debe crear un tercer conjunto que sea la unión teórica de conjuntos para los dos conjuntos existentes (es decir, un elemento del tercer arreglo se establece en true si ese elemento es true en cualquiera o en ambos de los conjuntos existentes; en caso contrario, el elemento del tercer conjunto se establece en false). El método interseccion debe crear un tercer conjunto que sea la intersección teórica de conjuntos para los dos conjuntos existentes (es decir, un elemento del arreglo del tercer conjunto se establece en false si ese elemento es false en uno o ambos de los conjuntos existentes; en caso contrario, el elemento del tercer conjunto se establece en true). El método insertarElemento debe insertar un nuevo entero k en un conjunto (estableciendo a[k] en true). El método eliminarElemento debe eliminar el entero m (estableciendo a[m] en false). El método aStringConjunto debe devolver una cadena que contenga un conjunto como una lista de números separados por espacios. Incluya sólo aquellos elementos que estén presentes en el conjunto. Use - - - para representar un conjunto vacío. El método esIgualA debe determinar si dos conjuntos son iguales. Escriba un programa para probar la clase ConjuntoEnteros. Cree instancias de varios objetos ConjuntoEnteros. Pruebe que todos sus métodos funcionen correctamente. 8.16

(Clase Fecha) Cree la clase Fecha con las siguientes capacidades: a) Imprimir la fecha en varios formatos, como MM/DD/AAAA Junio 15, 1992 DDD AAAA

b) Usar constructores sobrecargados para crear objetos Fecha inicializados con fechas de los formatos en la parte (a). En el primer caso, el constructor debe recibir tres valores enteros. En el segundo caso, debe recibir un objeto String y dos valores enteros. En el tercer caso debe recibir dos valores enteros, el primero de los cuales representa el número de día en el año. [Sugerencia: para convertir la representación de cadena del mes a un valor numérico, compare las cadenas usando el método equals. Por ejemplo, si s1 y s2 son cadenas, la llamada al método s1.equals( s2 ) devuelve true si las cadenas son idénticas y devuelve false en cualquier otro caso]. 8.17 (Números racionales) Cree una clase llamada Racional para realizar operaciones aritméticas con fracciones. Escriba un programa para probar su clase. Use variables enteras para representar las variables de instancia private de la clase: el numerador y el denominador. Proporcione un constructor que permita a un objeto de esta clase inicializarse al ser declarado. El constructor debe almacenar la fracción en forma reducida. La fracción 2/4

es equivalente a 1/2 y debe guardarse en el objeto como 1 en el numerador y 2 en el denominador. Proporcione un constructor sin argumentos con valores predeterminados, en caso de que no se proporcionen inicializadores. Proporcione métodos public que realicen cada una de las siguientes operaciones: a) Sumar dos números Racional: el resultado de la suma debe almacenarse en forma reducida. b) Restar dos números Racional: el resultado de la resta debe almacenarse en forma reducida. c) Multiplicar dos números Racional: el resultado de la multiplicación debe almacenarse en forma reducida. d) Dividir dos números Racional: el resultado de la división debe almacenarse en forma reducida. e) Imprimir números Racional en la forma a/b, en donde a es el numerador y b es el denominador. f ) Imprimir números Racional en formato de punto flotante. (Considere proporcionar capacidades de formato, que permitan al usuario de la clase especificar el número de dígitos de precisión a la derecha del punto decimal). 8.18 (Clase Entero Enorme) Cree una clase llamada EnteroEnorme que utilice un arreglo de 40 elementos de dígitos, para guardar enteros de hasta 40 dígitos de longitud cada uno. Proporcione los métodos entrada, salida, sumar y restar. Para comparar objetos EnteroEnorme, proporcione los siguientes métodos: esIgualA, noEsIgualA, esMayorQue, esMenorQue, esMayorOIgualA, y esMenorOIgualA. Cada uno de estos métodos deberá ser un método predicado que devuelva true si la relación se aplica entre los dos objetos EnteroEnorme, y false si no se aplica. Proporcione un método predicado llamado esCero. Si desea hacer algo más, proporcione también los métodos multiplicar, dividir y residuo. [Nota: los valores boolean primitivos pueden imprimirse como la palabra “true” o la palabra “false”, con el especificador de formato %b].

www.elsolucionario.net

Ejercicios

377

8.19 (Tres en raya) Cree una clase llamada TresEnRaya que le permita escribir un programa completo para jugar al “tres en raya” (o tres en línea). La clase debe contener un arreglo privado bidimensional de enteros, con un tamaño de 3 por 3. El constructor debe inicializar el tablero vacío con ceros. Permita dos jugadores humanos. Siempre que el primer jugador realice un movimiento, coloque un 1 en el cuadro especificado; coloque un 2 siempre que el segundo jugador realice un movimiento. Cada movimiento debe hacerse en un cuadro vacío. Después de cada movimiento, determine si el juego se ha ganado o si hay un empate. Si desea hacer algo más, modifique su programa de manera que la computadora realice los movimientos para uno de los jugadores. Además, permita que el jugador especifique si desea el primer o segundo turno. Si se siente todavía más motivado, desarrolle un programa que reproduzca un juego de Tres en raya tridimensional, en un tablero de 4 por 4 por 4 [Nota: ¡éste es un proyecto retador que podría requerir de muchas semanas de esfuerzo!].

www.elsolucionario.net

9 Programación orientada a objetos: herencia

No digas que conoces a alguien por completo, hasta que tengas que dividir una herencia con él. —Johann Kasper Lavater

Este método es para definirse como el número de la clase de todas las clases similares a la clase dada.

OBJETIVOS En este capítulo aprenderá a: Q

Comprender cómo la herencia fomenta la reutilización de software.

Q

Entender qué son las superclases y las subclases.

Q

Utilizar la palabra clave extends para crear una clase que herede los atributos y comportamientos de otra clase.

Q

Comprender el uso del modificador de acceso protected para dar a los métodos de la subclase acceso a los miembros de la superclase.

Preserva la autoridad base de los libros de otros.

Q

Utilizar los miembros de superclases mediante super.

—William Shakespeare

Q

Comprender cómo se utilizan los constructores en las jerarquías de herencia.

Q

Conocer los métodos de la clase Object, la superclase directa o indirecta de todas las clases en Java.

www.elsolucionario.net

—Bertrand Russell

Es bueno heredar una biblioteca, pero es mejor coleccionar una. —Augustine Birrell

Pla n g e ne r a l

9.1 Introducción

9.1 9.2 9.3 9.4

9.5 9.6 9.7 9.8 9.9

379

Introducción Superclases y subclases Miembros protected Relación entre las superclases y las subclases 9.4.1 Creación y uso de una clase EmpleadoPorComision 9.4.2 Creación de una clase EmpleadoBaseMasComision sin usar la herencia 9.4.3 Creación de una jerarquía de herencia EmpleadoPorComision-EmpleadoBaseMasComision 9.4.4 La jerarquía de herencia EmpleadoPorComision-EmpleadoBaseMasComision mediante el uso de variables de instancia protected 9.4.5 La jerarquía de herencia EmpleadoPorComision-EmpleadoBaseMasComision mediante el uso de variables de instancia private Los constructores en las subclases Ingeniería de software mediante la herencia La clase Object (Opcional) Ejemplo práctico de GUI y gráficos: mostar texto e imágenes usando etiquetas Conclusión

Resumen | Terminología | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

9.1 Introducción En este capítulo continuamos nuestra discusión acerca de la programación orientada a objetos (POO), introduciendo una de sus características principales, la herencia, que es una forma de reutilización de software en la que se crea una nueva clase absorbiendo los miembros de una clase existente, y se mejoran con nuevas capacidades, o con modificaciones en las capacidades ya existentes. Con la herencia, los programadores ahorran tiempo durante el desarrollo, al reutilizar software probado y depurado de alta calidad. Esto también aumenta la probabilidad de que un sistema se implemente con efectividad. Al crear una clase, en vez de declarar miembros completamente nuevos, el programador puede designar que la nueva clase herede los miembros de una clase existente. Esta clase existente se conoce como superclase, y la nueva clase se conoce como subclase. (El lenguaje de programación C++ se refieren a la superclase como la clase base, y a la subclase como clase derivada). Cada subclase puede convertirse en la superclase de futuras subclases. Una subclase generalmente agrega sus propios campos y métodos. Por lo tanto, una subclase es más específica que su superclase y representa a un grupo más especializado de objetos. Generalmente, la subclase exhibe los comportamientos de su superclase junto con comportamientos adicionales específicos de esta subclase. Es por ello que a la herencia se le conoce algunas veces como especialización. La superclase directa es la superclase a partir de la cual la subclase hereda en forma explícita. Una superclase indirecta es cualquier clase arriba de la superclase directa en la jerarquía de clases, la cual define las relaciones de herencia entre las clases. En Java, la jerarquía de clases empieza con la clase Object (en el paquete java.lang), a partir de la cual se extienden (o “heredan”) todas las clases en Java, ya sea en forma directa o indirecta. La sección 9.7 lista los métodos de la clase Object, de la cual heredan todas las demás clases. En el caso de la herencia simple, una clase se deriva de una superclase directa. Java, a diferencia de C++, no soporta la herencia múltiple (que ocurre cuando una clase se deriva de más de una superclase directa). En el capítulo 10, Programación orientada a objetos: polimorfismo, explicaremos cómo los programadores en Java pueden usar las interfaces para obtener muchos de los beneficios de la herencia múltiple, evitando al mismo tiempo los problemas asociados. La experiencia en la creación de sistemas de software nos indica que algunas cantidades considerables de código tratan con casos especiales, estrechamente relacionados. Cuando los programadores se preocupan con casos especiales, los detalles pueden oscurecer el panorama general. Con la programación orientada a objetos, los programadores se enfocan en los elementos comunes entre los objetos en el sistema, en vez de enfocarse en los casos especiales. Es necesario hacer una diferencia entre la relación “es un” y la relación “tiene un”. La relación “es un” representa a la herencia. En este tipo de relación, un objeto de una subclase puede tratarse también como un objeto de

www.elsolucionario.net

380

Capítulo 9

Programación orientada a objetos: herencia

su superclase. Por ejemplo, un automóvil es un vehículo. En contraste, la relación “tiene un” identifica a la composición (vea el capítulo 8). En este tipo de relación, un objeto contiene referencias a objetos como miembros. Por ejemplo, un automóvil tiene un volante de dirección (y un objeto automóvil tiene una referencia a un objeto volante de dirección). Las clases nuevas pueden heredar de las clases en las bibliotecas de clases. Las organizaciones desarrollan sus propias bibliotecas de clases y pueden aprovechar las que ya están disponibles en todo el mundo. Es probable que algún día, la mayoría de software nuevo se construya a partir de componentes reutilizables estándar, como sucede actualmente con la mayoría de los automóviles y del hardware de computadora. Esto facilitará el desarrollo de software más poderoso, abundante y económico.

9.2 Superclases y subclases A menudo, un objeto de una clase “es un” objeto de otra clase también. Por ejemplo, en la geometría un rectángulo es un cuadrilátero (al igual que los cuadrados, los paralelogramos y los trapezoides). Por lo tanto, en Java puede decirse que la clase Rectangulo hereda de la clase Cuadrilatero. En este contexto, la clase Cuadrilatero es una superclase, y la clase Rectangulo es una subclase. Un rectángulo es un tipo específico de cuadrilátero, pero es incorrecto decir que todo cuadrilátero es un rectángulo; el cuadrilátero podría ser un paralelogramo o alguna otra figura. En la figura 9.1 se muestran varios ejemplos sencillos de superclases y subclases; observe que las superclases tienden a ser “más generales”, y las subclases “más específicas”. Como todo objeto de una subclase “es un” objeto de su superclase, y como una superclase puede tener muchas subclases, el conjunto de objetos representados por una superclase generalmente es más grande que el conjunto de objetos representado por cualquiera de sus subclases. Por ejemplo, la superclase Vehiculo representa a todos los vehículos, incluyendo automóviles, camiones, barcos, bicicletas, etcétera. En contraste, la subclase Auto representa a un subconjunto más pequeño y específico de los vehículos. Las relaciones de herencia forman estructuras jerárquicas en forma de árbol. Una superclase existe en una relación jerárquica con sus subclases. Cuando las clases participan en relaciones de herencia, se “afilian” con otras clases. Una clase se convierte ya sea en una superclase, proporcionando miembros a otras clases, o en una subclase, heredando sus miembros de otras clases. En algunos casos, una clase es tanto superclase como subclase. Desarrollaremos una jerarquía de clases de ejemplo (figura 9.2), también conocida como jerarquía de herencia. Una comunidad universitaria tiene miles de miembros, compuestos por empleados, estudiantes y exalumnos. Los empleados pueden ser miembros del cuerpo docente o administrativo. Los miembros del cuerpo docente pueden ser administradores (como decanos o jefes de departamento) o maestros. Observe que la jerarquía podría contener muchas otras clases. Por ejemplo, los estudiantes pueden ser graduados o no graduados. Los no graduados pueden ser de primero, segundo, tercero o cuarto año. Cada flecha en la jerarquía representa una relación “es un”. Por ejemplo, al seguir las flechas en esta jerarquía de clases podemos decir “un Empleado es un MiembroDeLaComunidad” y “un Maestro es un miembro Docente”. MiembroDeLaComunidad es la superclase directa de Empleado, Estudiante y Exalumno, y es una superclase indirecta de todas las demás clases en el diagrama. Si comienza desde la parte inferior del diagrama, podrá seguir las flechas y aplicar la relación es-un hasta la superclase superior. Por ejemplo, un Administrador es un miembro Docente, es un Empleado y es un MiembroDeLaComunidad. Ahora considere la jerarquía de herencia de Figura en la figura 9.3. Esta jerarquía empieza con la superclase Figura, la cual se extiende mediante las subclases FiguraBidimensional y FiguraTridimensional; las Figu-

Superclase

Subclases

Estudiante

EstudianteGraduado, EstudianteNoGraduado.

Figura

Circulo, Triangulo, Rectangulo.

Prestamo

PrestamoAutomovil, PrestamoMejoraCasa, PrestamoHipotecario.

Empleado

Docente, Administrativo.

CuentaBancaria

CuentaDeCheques, CuentaDeAhorros.

Figura 9.1 | Ejemplos de herencia.

www.elsolucionario.net

9.2 Superclases y subclases

381

MiembroDeLaComunidad

Empleado

Docente

Administrador

Estudiante

Exalumno

Administrativo

Maestro

Figura 9.2 | Jerarquía de herencia para objetos MiembroDeLaComunidad universitaria.

Figura

FiguraBidimensional

Circulo

Cuadrado

FiguraTridimensional

Triangulo

Esfera

Cubo

Tetraedro

Figura 9.3 | Jerarquía de herencia para Figuras. son del tipo FiguraBidimensional o FiguraTridimensional. El tercer nivel de esta jerarquía contiene algunos tipos más específicos de figuras tipo FiguraBidimensional y FiguraTridimensional. Al igual que en la figura 9.2, podemos seguir las flechas desde la parte inferior del diagrama, hasta la superclase de más arriba en esta jerarquía de clases, para identificar varias relaciones es un. Por ejemplo, un Triangulo es un objeto FiguraBidimensional y es una Figura, mientras que una Esfera es una FiguraTridimensional y es una Figura. Observe que esta jerarquía podría contener muchas otras clases. Por ejemplo, las elipses y los trapezoides son del tipo FiguraBidimensional. No todas las relaciones de clases son una relación de herencia. En el capítulo 8 hablamos sobre la relación tiene-un, en la que las clases tienen miembros que hacen referencia a los objetos de otras clases. Tales relaciones crean clases mediante la composición de clases existentes. Por ejemplo, dadas las clases Empleado, FechaDeNacimiento y NumeroTelefonico, no es apropiado decir que un Empleado es una FechaDeNacimiento o que un Empleado es un NumeroTelefonico. Sin embargo, un Empleado tiene una FechaDeNacimiento y también tiene un NumeroTelefonico. Es posible tratar a los objetos de superclases y a los objetos de subclases de manera similar; sus similitudes se expresan en los miembros de la superclase. Los objetos de todas las clases que extienden a una superclase común pueden tratarse como objetos de esa superclase (es decir, dichos objetos tienen una relación “es un” con la superclase). Sin embargo, los objetos de una superclase no pueden tratarse como objetos de sus subclases. Por ejemplo, todos los automóviles son vehículos pero no todos los vehículos son automóviles (los otros vehículos podrían ser camiones, aviones o bicicletas, por ejemplo). Más adelante en este capítulo y en el 10, Programación orientada a objetos: polimorfismo, consideraremos muchos ejemplos que aprovechan la relación es un. Un problema con la herencia es que una subclase puede heredar métodos que no necesita, o que no debe tener. A pesar de que un método de superclase sea apropiado para una subclase, a menudo esa subclase requiere una versión personalizada del método. En dichos casos, la subclase puede sobrescribir (redefinir) el método de la superclase con una implementación apropiada, como veremos a menudo en los ejemplos de código de este capítulo. ras

www.elsolucionario.net

382

Capítulo 9

Programación orientada a objetos: herencia

9.3 Miembros protected

En el capítulo 8 hablamos sobre los modificadores de acceso public y private. Los miembros public de una clase son accesibles en cualquier parte en donde el programa tenga una referencia a un objeto de esa clase, o una de sus subclases. Los miembros private de una clase son accesibles sólo dentro de la misma clase. Los miembros private de una superclase no son heredados por sus subclases. En esta sección presentaremos el modificador de acceso protected. El uso del acceso protected ofrece un nivel intermedio de acceso entre public y private. Los miembros protected de una superclase pueden ser utilizados por los miembros de esa superclase, por los miembros de sus subclases y por los miembros de otras clases en el mismo paquete (los miembros protected también tienen acceso a nivel de paquete). Todos los miembros public y protected de una superclase retienen su modificador de acceso original cuando se convierten en miembros de la subclase (por ejemplo, los miembros public de la superclase se convierten en miembros public de la subclase, y los miembros protected de la superclase se convierten en miembros protected de la subclase). Los métodos de una subclase pueden referirse a los miembros public y protected que se hereden de la superclase con sólo utilizar los nombres de los miembros. Cuando un método de la subclase sobrescribe al método de la superclase, éste último puede utilizarse desde la subclase si se antepone a su nombre la palabra clave super y un punto (.). En la sección 9.4 hablaremos sobre el acceso a los miembros sobrescritos de la superclase.

Observación de ingeniería de software 9.1 Los métodos de una subclase no pueden tener acceso directo a los miembros private de su superclase. Una subclase puede modificar el estado de las variables de instancia private de la superclase sólo a través de los métodos que no sean private, que se proporcionan en la superclase y son heredados por la subclase.

Observación de ingeniería de software 9.2 Declarar variables de instancia private ayuda a los programadores a probar, depurar y modificar correctamente los sistemas. Si una subclase puede acceder a las variables de instancia private de su superclase, las clases que hereden de esa subclase podrían acceder a las variables de instancia también. Esto propagaría el acceso a las que deberían ser variables de instancia private, y se perderían los beneficios del ocultamiento de la información.

9.4 Relación entre las superclases y las subclases En esta sección usaremos una jerarquía de herencia que contiene tipos de empleados en la aplicación de nómina de una compañía, para hablar sobre la relación entre una superclase y su subclase. En esta compañía, a los empleados por comisión (que se representarán como objetos de una superclase) se les paga un porcentaje de sus ventas, mientras que los empleados por comisión con salario base (que se representarán como objetos de una subclase) reciben un salario base, más un porcentaje de sus ventas. Dividiremos nuestra discusión sobre la relación entre los empleados por comisión y los empleados por comisión con salario base en cinco ejemplos. El primero declara la clase EmpleadoPorComision, la cual hereda directamente de la clase Object y declara como variables de instancia private el primer nombre, el apellido paterno, el número de seguro social, la tarifa de comisión y el monto de ventas en bruto (es decir, total). El segundo ejemplo declara la clase EmpleadoBaseMasComision, la cual también hereda directamente de la clase Object y declara como variables de instancia private el primer nombre, el apellido paterno, el número de seguro social, la tarifa de comisión, el monto de ventas en bruto y el salario base. Para crear esta última clase, escribiremos cada línea de código que ésta requiera; pronto veremos que es mucho más eficiente crear esta clase haciendo que herede de la clase EmpleadoPorComision. El tercer ejemplo declara una clase EmpleadoBaseMasComision2 separada, la cual extiende a la clase EmpleadoPorComision (es decir, un EmpleadoBasePorComision2 es un EmpleadoPorComision que también tiene un salario base) y trata de acceder a los miembros private de la clase EmpleadoPorComision; esto produce errores de compilación, ya que la subclase no puede acceder a las variables de instancia private de la superclase. El cuarto ejemplo muestra que si las variables de instancia de EmpleadoPorComision se declaran como protected, una clase EmpleadoBaseMasComision3 que extiende a la clase EmpleadoPorComision2 puede acceder a los datos de manera directa. Para este fin, declaramos la clase EmpleadoPorComision2 con variables de instancia

www.elsolucionario.net

9.4 Relación entre las superclases y las subclases

383

protected. Todas las clases EmpleadoBaseMasComision contienen una funcionalidad idéntica, pero le mostraremos que la clase EmpleadoBaseMasComision3 es más fácil de crear y de manipular. Una vez que hablemos sobre la conveniencia de utilizar variables de instancia protected, crearemos el quinto ejemplo, el cual establece las variables de instancia de EmpleadoPorComision de vuelta a private en la clase EmpleadoPorComision3, para hacer cumplir la buena ingeniería de software. Después le mostraremos cómo una clase EmpleadoBaseMasComision4 separada, que extiende a la clase EmpleadoPorComision3, puede utilizar los métodos public de EmpleadoPorComision3 para manipular las variables de instancia private de EmpleadoPorComision3.

9.4.1 Creación y uso de una clase EmpleadoPorComision Comenzaremos por declarar la clase EmpleadoPorComision (figura 9.4). La línea 4 empieza la declaración de la clase, e indica que la clase EmpleadoPorComision extiende (extends) (es decir, hereda de) la clase Object (del paquete java.lang). Los programadores de Java utilizan la herencia para crear clases a partir de clases existentes. De hecho, todas las clases en Java (excepto Object) extienden a una clase existente. Como la clase EmpleadoPorComision extiende la clase Object, la clase EmpleadoPorComision hereda los métodos de la clase Object; la clase Object no tiene campos. De hecho, cada clase en Java hereda en forma directa o indirecta los métodos de Object. Si una clase no especifica que extiende a otra clase, la nueva clase hereda de Object en forma implícita. Por esta razón, es común que los programadores no incluyan “extends Object” en su código; en nuestro ejemplo lo haremos sólo por fines demostrativos.

Observación de ingeniería de software 9.3 El compilador de Java establece la superclase de una clase a Object cuando la declaración de la clase no extiende explícitamente una superclase.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

// Fig. 9.4: EmpleadoPorComision.java // La clase EmpleadoPorComision representa a un empleado por comisión. public class EmpleadoPorComision extends Object { private String primerNombre; private String apellidoPaterno; private String numeroSeguroSocial; private double ventasBrutas; // ventas semanales totales private double tarifaComision; // porcentaje de comisión // constructor con cinco argumentos public EmpleadoPorComision( String nombre, String apellido, String nss, double ventas, double tarifa ) { // la llamada implícita al constructor del objeto ocurre aquí primerNombre = nombre; apellidoPaterno = apellido; numeroSeguroSocial = nss; establecerVentasBrutas( ventas ); // valida y almacena las ventas brutas establecerTarifaComision( tarifa ); // valida y almacena la tarifa de comisión } // fin del constructor de EmpleadoPorComision con cinco argumentos // establece el primer nombre public void establecerPrimerNombre( String nombre ) { primerNombre = nombre;

Figura 9.4 | La clase EmpleadoPorComision representa a un empleado que recibe como sueldo un porcentaje de las ventas brutas. (Parte 1 de 3).

www.elsolucionario.net

384

28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85

Capítulo 9

Programación orientada a objetos: herencia

} // fin del método establecerPrimerNombre // devuelve el primer nombre public String obtenerPrimerNombre() { return primerNombre; } // fin del método obtenerPrimerNombre // establece el apellido paterno public void establecerApellidoPaterno( String apellido ) { apellidoPaterno = apellido; } // fin del método establecerApellidoPaterno // devuelve el apellido paterno public String obtenerApellidoPaterno() { return apellidoPaterno; } // fin del método obtenerApellidoPaterno // establece el número de seguro social public void establecerNumeroSeguroSocial( String nss ) { numeroSeguroSocial = nss; // debe validar } // fin del método establecerNumeroSeguroSocial // devuelve el número de seguro social public String obtenerNumeroSeguroSocial() { return numeroSeguroSocial; } // fin del método obtenerNumeroSeguroSocial // establece el monto de ventas totales del empleado por comisión public void establecerVentasBrutas( double ventas ) { ventasBrutas = ( ventas < 0.0 ) ? 0.0 : ventas; } // fin del método establecerVentasBrutas // devuelve el monto de ventas totales del empleado por comisión public double obtenerVentasBrutas() { return ventasBrutas; } // fin del método obtenerVentasBrutas // establece la tarifa del empleado por comisión public void establecerTarifaComision( double tarifa ) { tarifaComision = ( tarifa > 0.0 && tarifa < 1.0 ) ? tarifa : 0.0; } // fin del método establecerTarifaComision // devuelve la tarifa del empleado por comisión public double obtenerTarifaComision() { return tarifaComision; } // fin del método obtenerTarifaComision // calcula el salario del empleado por comisión public double ingresos()

Figura 9.4 | La clase EmpleadoPorComision representa a un empleado que recibe como sueldo un porcentaje de las ventas brutas. (Parte 2 de 3).

www.elsolucionario.net

9.4 Relación entre las superclases y las subclases

86 87 88 89 90 91 92 93 94 95 96 97 98 99

385

{ return tarifaComision * ventasBrutas; } // fin del método ingresos // devuelve representación String del objeto EmpleadoPorComision public String toString()" { return String.format( "%s: %s %s\n%s: %s\n%s: %.2f\n%s: %.2f", "empleado por comision", primerNombre, apellidoPaterno, "numero de seguro social", numeroSeguroSocial, "ventas brutas", ventasBrutas, “tarifa de comision”, tarifaComision ); } // fin del método toString } // fin de la clase EmpleadoPorComision

Figura 9.4 | La clase EmpleadoPorComision representa a un empleado que recibe como sueldo un porcentaje de las ventas brutas. (Parte 3 de 3).

Los servicios public de la clase EmpleadoPorComision incluyen un constructor (líneas 13 a 22), y los métodos ingresos (líneas 85 a 88) y toString (líneas 91 a 98). Las líneas 25 a 82 declaran métodos establecer y obtener public para manipular las variables de instancia primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas y tarifaComision de la clase (las cuales se declaran en las líneas 6 a 10). La clase EmpleadoPorComision declara cada una de sus variables de instancia como private, por lo que los objetos de otras clases no pueden acceder directamente a estas variables. Declarar las variables de instancia como private y proporcionar métodos establecer y obtener para manipular y validar las variables de instancia ayuda a cumplir con la buena ingeniería de software. Por ejemplo, los métodos establecerVentasBrutas y establecerTarifaComision validan sus argumentos antes de asignar los valores a las variables de instancia ventasBrutas y tarifaComision, en forma respectiva. Los constructores no se heredan, por lo que la clase EmpleadoPorComision no hereda el constructor de la clase Object. Sin embargo, el constructor de la clase EmpleadoPorComision llama al constructor de la clase Object de manera implícita. De hecho, la primera tarea del constructor de cualquier subclase es llamar al constructor de su superclase directa, ya sea en forma explícita o implícita (si no se especifica una llamada al constructor), para asegurar que las variables de instancia heredadas de la superclase se inicialicen en forma apropiada. En la sección 9.4.3 hablaremos sobre la sintaxis para llamar al constructor de una superclase en forma explícita. Si el código no incluye una llamada explícita al constructor de la superclase, Java genera una llamada implícita al constructor predeterminado o sin argumentos de la superclase. El comentario en la línea 16 de la figura 9.4 indica en dónde se hace la llamada implícita al constructor predeterminado de la superclase Object (el programador no necesita escribir el código para esta llamada). El constructor predeterminado (vacío) de la clase Object no hace nada. Observe que aun si una clase no tiene constructores, el constructor predeterminado que declara el compilador de manera implícita para la clase llamará al constructor predeterminado o sin argumentos de la superclase. Una vez que se realiza la llamada implícita al constructor de Object, las líneas 17 a 21 del constructor de EmpleadoPorComision asignan valores a las variables de instancia de la clase. Observe que no validamos los valores de los argumentos nombre, apellido y nss antes de asignarlos a las variables de instancia correspondientes. Podríamos validar el nombre y el apellido; tal vez asegurarnos de que tengan una longitud razonable. De manera similar, podría validarse un número de seguro social, para asegurar que contenga nueve dígitos, con o sin guiones cortos (por ejemplo, 123-45-6789 o 123456789). El método ingresos (líneas 85 a 88) calcula los ingresos de un EmpleadoPorComision. La línea 87 multiplica la tarifaComision por las ventasBrutas y devuelve el resultado. El método toString (líneas 91 a 98) es especial: es uno de los métodos que hereda cualquier clase de manera directa o indirecta de la clase Object, la cual es la raíz de la jerarquía de clases de Java. La sección 9.7 muestra un resumen de los métodos de la clase Object. El método toString devuelve un String que representa a un objeto. Un programa llama a este método de manera implícita cada vez que un objeto debe convertirse en una representación de cadena, como cuando se imprime un objeto mediante printf o el método format de String, usando el especificador de formato %s. El método toString de la clase Object devuelve un String que incluye

www.elsolucionario.net

386

Capítulo 9

Programación orientada a objetos: herencia

el nombre de la clase del objeto. En esencia, es un receptáculo que puede sobrescribirse por una subclase para especificar una representación de cadena apropiada de los datos en un objeto de la subclase. El método toString de la clase EmpleadoPorComision sobrescribe (redefine) al método toString de la clase Object. Al invocarse, el método toString de EmpleadoPorComision usa el método String llamado format para devolver un String que contiene información acerca del EmpleadoPorComision. Para sobrescribir a un método de una superclase, una subclase debe declarar un método con la misma firma (nombre del método, número de parámetros, tipos de los parámetros y orden de los tipos de los parámetros) que el método de la superclase; el método toString de Object no recibe parámetros, por lo que EmpleadoPorComision declara a toString sin parámetros.

Error común de programación 9.1 Es un error de compilación sobrescribir un método con un modificador de acceso más restringido; un método public de la superclase no puede convertirse en un método protected o private en la subclase; un método protected de la superclase no puede convertirse en un método private en la subclase. Hacer esto sería quebrantar la relación es un, en la que se requiere que todos los objetos de la subclase puedan responder a las llamadas a métodos que se hagan a los métodos public declarados en la superclase. Si un método public pudiera sobrescribirse como protected o private, los objetos de la subclase no podrían responder a las mismas llamadas a métodos que los objetos de la superclase. Una vez que se declara un método como public en una superclase, el método sigue siendo public para todas las subclases directas e indirectas de esa clase.

La figura 9.5 prueba la clase EmpleadoPorComision. Las líneas 9 a 10 crean una instancia de un objeEmpleadoPorComision e invocan a su constructor (líneas 13 a 22 de la figura 9.4) para inicializarlo con "Sue" como el primer nombre, "Jones" como el apellido, "222-22-2222" como el número de seguro social, 10000 como el monto de ventas brutas y .06 como la tarifa de comisión. Las líneas 15 a 24 utilizan los métodos obtener de EmpleadoPorComision para obtener los valores de las variables de instancia del objeto e imprimirlas en pantalla. Las líneas 26 y 27 invocan a los métodos establecerVentasBrutas y establecerTarifaComision del objeto para modificar los valores de las variables de instancia ventasBrutas y tarifaComision. Las líneas 29 y 30 imprimen en pantalla la representación de cadena del EmpleadoPorComision actualizado. Observe que cuando se imprime un objeto en pantalla usando el especificador de formato %s, se invoca de manera implícita el método toString del objeto para obtener su representación de cadena. to

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

// Fig. 9.5: PruebaEmpleadoPorComision.java // Prueba de la clase EmpleadoPorComision. public class PruebaEmpleadoPorComision { public static void main( String args[] ) { // crea instancia de objeto EmpleadoPorComision EmpleadoPorComision empleado = new EmpleadoPorComision( "Sue", "Jones", "222-22-2222", 10000, .06 ); // obtiene datos del empleado por comisión System.out.println( "Informacion del empleado obtenida por los metodos establecer: \n" ); System.out.printf( "%s %s\n", "El primer nombre es", empleado.obtenerPrimerNombre() ); System.out.printf( "%s %s\n", "El apellido paterno es", empleado.obtenerApellidoPaterno() ); System.out.printf( "%s %s\n", "El numero de seguro social es", empleado.obtenerNumeroSeguroSocial() ); System.out.printf( "%s %.2f\n", "Las ventas brutas son", empleado.obtenerVentasBrutas() ); System.out.printf( "%s %.2f\n", "La tarifa de comision es", empleado.obtenerTarifaComision() );

Figura 9.5 | Programa de prueba de la clase EmpleadoPorComision. (Parte 1 de 2).

www.elsolucionario.net

9.4 Relación entre las superclases y las subclases

25 26 27 28 29 30 31 32

387

empleado.establecerVentasBrutas( 500 ); // establece las ventas brutas empleado.establecerTarifaComision( .1 ); // establece la tarifa de comisión System.out.printf( "\n%s:\n\n%s\n", "Informacion actualizada del empleado, obtenida mediante toString", empleado ); } // fin de main } // fin de la clase PruebaEmpleadoPorComision

Informacion del empleado obtenida por los metodos establecer: El primer nombre es Sue El apellido paterno es Jones El numero de seguro social es 222-22-2222 Las ventas brutas son 10000.00 La tarifa de comision es 0.06 Informacion actualizada del empleado, obtenida mediante toString: empleado por comision: Sue Jones numero de seguro social: 222-22-2222 ventas brutas: 500.00 tarifa de comision: 0.10

Figura 9.5 | Programa de prueba de la clase EmpleadoPorComision. (Parte 2 de 2).

9.4.2 Creación de una clase EmpleadoBaseMasComision sin usar la herencia Ahora hablaremos sobre la segunda parte de nuestra introducción a la herencia, mediante la declaración y prueba de la clase (completamente nueva e independiente) EmpleadoBaseMasComision (figura 9.6), la cual contiene los siguientes datos: primer nombre, apellido paterno, número de seguro social, monto de ventas brutas, tarifa de comisión y salario base. Los servicios public de la clase EmpleadoBaseMasComision incluyen un constructor (líneas 15 a 25), y los métodos ingresos (líneas 100 a 103) y toString (líneas 106 a 114). Las líneas 28 a 97 declaran métodos establecer y obtener public para las variables de instancia private primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas, tarifaComision y salarioBase para la clase (las cuales se declaran en las líneas 7 a 12). Estas variables y métodos encapsulan todas las características necesarias de un empleado por comisión con sueldo base. Observe la similitud entre esta clase y la clase EmpleadoPorComision (figura 9.4); en este ejemplo, no explotaremos todavía esa similitud.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

// Fig. 9.6: EmpleadoBaseMasComision.java // La clase EmpleadoBaseMasComision representa a un empleado que recibe // un salario base, además de la comisión. public class EmpleadoBaseMasComision { private String primerNombre; private String apellidoPaterno; private String numeroSeguroSocial; private double ventasBrutas; // ventas totales por semana private double tarifaComision; // porcentaje de comisión private double salarioBase; // salario base por semana // constructor con seis argumentos public EmpleadoBaseMasComision( String nombre, String apellido, String nss, double ventas, double tarifa, double salario ) {

Figura 9.6 | La clase EmpleadoBaseMasComision representa a un empleado que recibe un salario base, además de la comisión. (Parte 1 de 3).

www.elsolucionario.net

388

18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75

Capítulo 9

Programación orientada a objetos: herencia

// la llamada implícita al constructor de Object ocurre aquí primerNombre = nombre; apellidoPaterno = apellido; numeroSeguroSocial = nss; establecerVentasBrutas( ventas ); // valida y almacena las ventas brutas establecerTarifaComision( tarifa ); // valida y almacena la tarifa de comisión establecerSalarioBase( salario ); // valida y almacena el salario base } // fin del constructor de EmpleadoBaseMasComision con seis argumentos // establece el primer nombre public void establecerPrimerNombre( String nombre ) { primerNombre = nombre; } // fin del método establecerPrimerNombre // devuelve el primer nombre public String obtenerPrimerNombre() { return primerNombre; } // fin del método obtenerPrimerNombre // establece el apellido paterno public void establecerApellidoPaterno( String apellido ) { apellidoPaterno = apellido; } // fin del método establecerApellidoPaterno // devuelve el apellido paterno public String obtenerApellidoPaterno() { return apellidoPaterno; } // fin del método obtenerApellidoPaterno // establece el número de seguro social public void establecerNumeroSeguroSocial( String nss ) { numeroSeguroSocial = nss; // debe validar } // fin del método establecerNumeroSeguroSocial // devuelve el número de seguro social public String obtenerNumeroSeguroSocial() { return numeroSeguroSocial; } // fin del método obtenerNumeroSeguroSocial // establece el monto de ventas brutas public void establecerVentasBrutas( double ventas ) { ventasBrutas = ( ventas < 0.0 ) ? 0.0 : ventas; } // fin del método establecerVentasBrutas // devuelve el monto de ventas brutas public double obtenerVentasBrutas() { return ventasBrutas; } // fin del método obtenerVentasBrutas // establece la tarifa de comisión

Figura 9.6 | La clase EmpleadoBaseMasComision representa a un empleado que recibe un salario base, además de la comisión. (Parte 2 de 3).

www.elsolucionario.net

9.4 Relación entre las superclases y las subclases

76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115

389

public void establecerTarifaComision( double tarifa ) { tarifaComision = ( tarifa > 0.0 && tarifa < 1.0 ) ? tarifa : 0.0; } // fin del método establecerTarifaComision // devuelve la tarifa de comisión public double obtenerTarifaComision() { return tarifaComision; } // fin del método obtenerTarifaComision // establece el salario base public void establecerSalarioBase( double salario ) { salarioBase = ( salario < 0.0 ) ? 0.0 : salario; } // fin del método establecerSalarioBase // devuelve el salario base public double obtenerSalarioBase() { return salarioBase; } // fin del método obtenerSalarioBase // calcula los ingresos public double ingresos() { return salarioBase + ( tarifaComision * ventasBrutas ); } //fin del método ingresos // devuelve representación String de EmpleadoBaseMasComision public String toString() { return String.format( "%s: %s %s\n%s: %s\n%s: %.2f\n%s: %.2f\n%s: %.2f", "empleado por comision con sueldo base", primerNombre, apellidoPaterno, "numero de seguro social", numeroSeguroSocial, "ventas brutas", ventasBrutas, "tarifa de comision", tarifaComision, "salario base", salarioBase ); } // fin del método toString } // fin de la clase EmpleadoBaseMasComision

Figura 9.6 | La clase EmpleadoBaseMasComision representa a un empleado que recibe un salario base, además de la comisión. (Parte 3 de 3).

Observe que la clase EmpleadoBaseMasComision no especifica “extends Object” en la línea 5, por lo que la clase extiende a Object en forma implícita. Observe además que, al igual que el constructor de la clase EmpleadoPorComision (líneas 13 a 22 de la figura 9.4), el constructor de la clase EmpleadoBaseMasComision invoca al constructor predeterminado de la clase Object en forma implícita, como se indica en el comentario de la línea 18. El método ingresos de la clase EmpleadoBaseMasComision (líneas 100 a 103) calcula los ingresos de un empleado por comisión con salario base. La línea 102 devuelve el resultado de sumar el salario base del empleado al producto de multiplicar la tarifa de comisión por las ventas brutas del empleado. La clase EmpleadoBaseMasComision sobrescribe al método toString de Object para que devuelva un objeto String que contiene la información del EmpleadoBaseMasComision. Una vez más, utilizamos el especificador de formato %.2f para dar formato a las ventas brutas, la tarifa de comisión y el salario base con dos dígitos de precisión a la derecha del punto decimal (línea 109).

www.elsolucionario.net

390

Capítulo 9

Programación orientada a objetos: herencia

La figura 9.7 prueba la clase EmpleadoBaseMasComision. Las líneas 9 a 11 crean una instancia de un objeto EmpleadoBaseMasComision y pasan los argumentos "Bob", "Lewis", "333-33-3333", 5000, .04 y 300 al constructor como el primer nombre, apellido paterno, número de seguro social, ventas brutas, tarifa de comisión y salario base, respectivamente. Las líneas 16 a 27 utilizan los métodos obtener de EmpleadoBaseMasComision para obtener los valores de las variables de instancia del objeto e imprimirlos en pantalla. La línea 29 invoca al método establecerSalarioBase del objeto para modificar el salario base. El método establecerSalarioBase (figura 9.6, líneas 88 a 91) asegura que no se le asigne a la variable salarioBase un valor negativo, ya que el salario base de un empleado no puede ser negativo. Las líneas 31 a 33 de la figura 9.7 invocan en forma implícita al método toString del objeto, para obtener su representación de cadena. La mayor parte del código para la clase EmpleadoBaseMasComision (figura 9.6) es similar, si no es que idéntico, al código para la clase EmpleadoPorComision (figura 9.4). Por ejemplo, en la clase EmpleadoBaseMasComision, las variables de instancia private primerNombre y apellidoPaterno, y los métodos establecerPrimerNombre, obtenerPrimerNombre, establecerApellidoPaterno y obtenerApellidoPaterno son idénticos a los de la clase EmpleadoPorComision. Las clases EmpleadoPorComision y EmpleadoBasePorComision también contienen las variables de instancia private numeroSeguroSocial, tarifaComision y ventasBrutas, así como métodos obtener y establecer para manipular estas variables. Además, el constructor de EmpleadoBasePorComision es casi idéntico al de la clase EmpleadoPorComision, sólo que el constructor de EmpleadoBaseMasComision también establece el salarioBase. Las demás adiciones a la clase EmpleadoBaseMasComision son la variable de instancia private salarioBase, y los métodos establecerSalarioBase

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

// Fig. 9.7: PruebaEmpleadoBaseMasComision.java // Prueba de la clases EmpleadoBaseMasComision. public class PruebaEmpleadoBaseMasComision { public static void main( String args[] ) { // crea instancia de objeto EmpleadoBaseMasComision EmpleadoBaseMasComision empleado = new EmpleadoBaseMasComision( "Bob", "Lewis", "333-33-3333", 5000, .04, 300 ); // obtiene datos del empleado por comisión con sueldo base System.out.println( "Informacion del empleado obtenida por metodos establecer: \n" ); System.out.printf( "%s %s\n", "El primer nombre es", empleado.obtenerPrimerNombre() ); System.out.printf( "%s %s\n", "El apellido es", empleado.obtenerApellidoPaterno() ); System.out.printf( "%s %s\n", "El numero de seguro social es", empleado.obtenerNumeroSeguroSocial() ); System.out.printf( "%s %.2f\n", "Las ventas brutas son”, empleado.obtenerVentasBrutas() ); System.out.printf( "%s %.2f\n", "La tarifa de comision es”, empleado.obtenerTarifaComision() ); System.out.printf( "%s %.2f\n", "El salario base es", empleado.obtenerSalarioBase() ); empleado.establecerSalarioBase( 1000 ); // establece el salario base System.out.printf( "\n%s:\n\n%s\n", "Informacion actualizada del empleado, obtenida por toString", empleado.toString() ); } // fin de main } // fin de la clase PruebaEmpleadoBaseMasComision

Figura 9.7 | Programa de prueba de EmpleadoBaseMasComision. (Parte 1 de 2).

www.elsolucionario.net

9.4 Relación entre las superclases y las subclases

391

Informacion del empleado obtenida por metodos establecer: El primer nombre es Bob El apellido es Lewis El numero de seguro social es 333-33-3333 Las ventas brutas son 5000.00 La tarifa de comision es 0.04 El salario base es 300.00 Informacion actualizada del empleado, obtenida por toString: empleado por comision con sueldo base: Bob Lewis numero de seguro social: 333-33-3333 ventas brutas: 5000.00 tarifa de comision: 0.04 salario base: 1000.00

Figura 9.7 | Programa de prueba de EmpleadoBaseMasComision. (Parte 2 de 2).

y obtenerSalarioBase. El método toString de la clase EmpleadoBaseMasComision es casi idéntico al de la clase EmpleadoPorComision, excepto que el método toString de EmpleadoBasePorComision también imprime la variable de instancia salarioBase con dos dígitos de precisión a la derecha del punto decimal. Literalmente hablando, copiamos el código de la clase EmpleadoPorComision y lo pegamos en la clase EmpleadoBaseMasComision, después modificamos esta clase para incluir un salario base y los métodos que manipulan ese salario base. A menudo, este método de “copiar y pegar” está propenso a errores y consume mucho tiempo. Peor aún, se pueden esparcir muchas copias físicas del mismo código a lo largo de un sistema, con lo que el mantenimiento del código se convierte en una pesadilla. ¿Existe alguna manera de “absorber” las variables de instancia y los métodos de una clase, de manera que formen parte de otras clases sin tener que copiar el código? En los siguientes ejemplos responderemos a esta pregunta, utilizando un método más elegante para crear clases, que enfatiza los beneficios de la herencia.

Observación de ingeniería de software 9.4 Copiar y pegar código de una clase a otra puede esparcir los errores a través de varios archivos de código fuente. Para evitar la duplicación de código (y posiblemente los errores) use la herencia o, en algunos casos, la composición, en vez del método de “copiar y pegar”, en situaciones en las que desea que una clase “absorba” las variables de instancia y los métodos de otra clase.

Observación de ingeniería de software 9.5 Con la herencia, las variables de instancia y los métodos comunes de todas las clases en la jerarquía se declaran en una superclase. Cuando se requieren modificaciones para estas características comunes, los desarrolladores de software sólo necesitan realizar las modificaciones en la superclase; así las clases derivadas heredan los cambios. Sin la herencia, habría que modificar todos los archivos de código fuente que contengan una copia del código en cuestión.

9.4.3 Creación de una jerarquía de herencia EmpleadoPorComision-EmpleadoBaseMasComision Ahora declararemos la clase EmpleadoBaseMasComision2 (figura 9.8), que extiende a la clase EmpleadoPorComision (figura 9.4). Un objeto EmpleadoBaseMasComision2 es un EmpleadoPorComision (ya que la herencia traspasa las capacidades de la clase EmpleadoPorComision), pero la clase EmpleadoBaseMasComision2 también tiene la variable de instancia salarioBase (figura 9.8, línea 6). La palabra clave extends en la línea 4 de la declaración de la clase indica la herencia. Como subclase, EmpleadoBaseMasComision2 hereda las variables de instancia public y protected y los métodos de la clase EmpleadoPorComision. El constructor de la clase EmpleadoPorComision no se hereda. Por lo tanto, los servicios public de EmpleadoBaseMasComision2 incluyen su constructor (líneas 9 a 16), los métodos public heredados de la clase EmpleadoPorComision, el

www.elsolucionario.net

392

Capítulo 9

Programación orientada a objetos: herencia

método establecerSalarioBase (líneas 19 a 22), el método obtenerSalarioBase (líneas 25 a 28), el método ingresos (líneas 31 a 35) y el método toString (líneas 38 a 47). El constructor de cada subclase debe llamar en forma implícita o explícita al constructor de su superclase, para asegurar que las variables de instancia heredadas de la superclase se inicialicen en forma apropiada. El constructor de EmpleadoBaseMasComision2 con seis argumentos (líneas 9 a 16) llama en forma explícita al constructor de la clase EmpleadoPorComision con cinco argumentos, para inicializar la porción correspondiente a la superclase de un objeto EmpleadoBaseMasComision2 (es decir, las variables primerNombre, apellidoPaterno, numero-

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

// Fig. 9.8: EmpleadoBaseMasComision2.java // EmpleadoBaseMasComision2 hereda de la clase EmpleadoPorComision. public class EmpleadoBaseMasComision2 extends EmpleadoPorComision { private double salarioBase; // salario base por semana // constructor con seis argumentos public EmpleadoBaseMasComision2( String nombre, String apellido, String nss, double ventas, double tarifa, double salario ) { // llamada explícita al constructor de la superclase EmpleadoPorComision super( nombre, apellido, nss, ventas, tarifa ); establecerSalarioBase( salario ); // valida y almacena el salario base } // fin del constructor de EmpleadoBaseMasComision2 con seis argumentos // establecer salario base public void establecerSalarioBase( double salario ) { salarioBase = ( salario < 0.0 ) ? 0.0 : salario; } // fin del método establecerSalarioBase // devuelve el salario base public double obtenerSalarioBase() { return salarioBase; } // fin del método obtenerSalarioBase // calcula los ingresos public double ingresos() { // no está permitido: tarifaComision y ventasBrutas son private en la superclase return salarioBase + ( tarifaComision * ventasBrutas ); } // fin del método ingresos // devuelve representación String de EmpleadoBaseMasComision2 public String toString() { // no está permitido: intentos por acceder a los miembros private de la superclase return String.format( "%s: %s %s\n%s: %s\n%s: %.2f\n%s: %.2f\n%s: %.2f", "empleado por comision con sueldo base", primerNombre, apellidoPaterno, "numero de seguro social", numeroSeguroSocial, "ventas brutas", ventasBrutas, "tarifa de comision", tarifaComision, "salario base", salarioBase ); } // fin del método toString } // fin de la clase EmpleadoBaseMasComision2

Figura 9.8 | Los miembros private de una superclase no se pueden utilizar en una subclase. (Parte 1 de 2).

www.elsolucionario.net

9.4 Relación entre las superclases y las subclases

393

EmpleadoBaseMasComision2.java:34: tarifaComision has private access in EmpleadoPorComision return salarioBase + ( tarifaComision * ventasBrutas ); ^ EmpleadoBaseMasComision2.java:34: ventasBrutas has private access in EmpleadoPorComision return salarioBase + ( tarifaComision * ventasBrutas ); ^ EmpleadoBaseMasComision2.java:43: primerNombre has private access in EmpleadoPorComision "empleado por comision con sueldo base", primerNombre, apellidoPaterno, ^ EmpleadoBaseMasComision2.java:43: apellidoPaterno has private access in EmpleadoPorComision "empleado por comision con sueldo base", primerNombre, apellidoPaterno, ^ EmpleadoBaseMasComision2.java:44: numeroSeguroSocial has private access in EmpleadoPorComision "numero de seguro social", numeroSeguroSocial, ^ EmpleadoBaseMasComision2.java:45: ventasBrutas has private access in EmpleadoPorComision "ventas brutas", ventasBrutas, "tarifa de comision", tarifaComision, ^ EmpleadoBaseMasComision2.java:45: tarifaComision has private access in EmpleadoPorComision "ventas brutas", ventasBrutas, "tarifa de comision", tarifaComision, ^ 7 errors

Figura 9.8 | Los miembros private de una superclase no se pueden utilizar en una subclase. (Parte 2 de 2). SeguroSocial, ventasBrutas y tarifaComision). La línea 13 en el constructor de EmpleadoBaseMasComision2 con seis argumentos invoca al constructor de EmpleadoPorComision con cinco argumentos (declarado

en las líneas 13 a 22 de la figura 9.4) mediante el uso de la sintaxis de llamada al constructor de la superclase: la palabra clave super, seguida de un conjunto de paréntesis que contienen los argumentos del constructor de la superclase. Los argumentos nombre, apellido, nss, ventas y tarifa se utilizan para inicializar a los miembros primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas y tarifaComision de la superclase, respectivamente. Si el constructor de EmpleadoBaseMasComision2 no invocara al constructor de EmpleadoPorComision de manera explícita, Java trataría de invocar al constructor predeterminado o sin argumentos de la clase EmpleadoPorComision; pero como la clase no tiene un constructor así, el compilador generaría un error. La llamada explícita al constructor de la superclase en la línea 13 de la figura 9.8 debe ser la primera instrucción en el cuerpo del constructor de la subclase. Además, cuando una superclase contiene un constructor sin argumentos, puede usar a super() para llamar a ese constructor en forma explícita, pero esto se hace raras veces.

Error común de programación 9.2 Si el constructor de una subclase llama a uno de los constructores de su superclase con argumentos que no concuerdan exactamente con el número y el tipo de los parámetros especificados en una de las declaraciones del constructor de la clase base, se produce un error de compilación.

El compilador genera errores para la línea 34 de la figura 9.8, debido a que las variables de instancia tarifaComision y ventasBrutas de la superclase EmpleadoPorComision son private; no se permite a los métodos de la subclase EmpleadoBaseMasComision2 acceder a las variables de instancia private de la superclase EmpleadoPorComision. Observe que utilizamos texto en negritas en la figura 9.8 para indicar que el código es erróneo. El compilador genera errores adicionales en las líneas 43 a 45 del método toString de EmpleadoBaseMasComision2 por la misma razón. Se hubieran podido prevenir los errores en EmpleadoBaseMasComision2 al utilizar

www.elsolucionario.net

394

Capítulo 9

Programación orientada a objetos: herencia

los métodos obtener heredados de la clase EmpleadoPorComision. Por ejemplo, la línea 34 podría haber utilizado obtenerTarifaComision y obtenerVentasBrutas para acceder a las variables de instancia private tarifaComision y ventasBrutas de EmpleadoPorComision, respectivamente. Las líneas 43 a 45 también podrían haber utilizado métodos establecer apropiados para obtener los valores de las variables de instancia de la superclase.

9.4.4 La jerarquía de herencia EmpleadoPorComisionEmpleadoBaseMasComision mediante el uso de variables de instancia protected Para permitir que la clase EmpleadoBaseMasComision acceda directamente a las variables de instancia primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas y tarifaComision de la superclase, podemos declarar esos miembros como protected en la superclase. Como vimos en la sección 9.3, los miembros protected de una superclase se heredan por todas las subclases de esa superclase. La clase EmpleadoPorComision2 (figura 9.9) es una modificación de la clase EmpleadoPorComision (figura 9.4), la cual declara las variables de instancia primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas y tarifaComision como protected, en vez de private (figura 9.9, líneas 6 a 10). Aparte del cambio en el nombre de la clase (y por ende el cambio en el nombre del constructor) a EmpleadoPorComision2, el resto de la declaración de la clase en la figura 9.9 es idéntico al de la figura 9.4.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

// Fig. 9.9: EmpleadoPorComision2.java // La clase EmpleadoPorComision2 representa a un empleado por comisión. public class { protected protected protected protected protected

EmpleadoPorComision2 String String String double double

primerNombre; apellidoPaterno; numeroSeguroSocial; ventasBrutas; // ventas totales por semana tarifaComision; // porcentaje de comisión

// constructor con cinco argumentos public EmpleadoPorComision2( String nombre, String apellido, String nss, double ventas, double tarifa ) { // la llamada implícita al constructor del objeto ocurre aquí primerNombre = nombre; apellidoPaterno = apellido; numeroSeguroSocial = nss; establecerVentasBrutas( ventas ); // valida y almacena las ventas brutas establecerTarifaComision( tarifa ); // valida y almacena la tarifa de comisión } // fin del constructor de EmpleadoPorComision2 con cinco argumentos // establece el primer nombre public void establecerPrimerNombre( String nombre ) { primerNombre = nombre; } // fin del método establecerPrimerNombre // devuelve el primer nombre public String obtenerPrimerNombre() { return primerNombre; } // fin del método obtenerPrimerNombre // establece el apellido paterno

Figura 9.9 |

EmpleadoPorComision2

con variables de instancia protected. (Parte 1 de 3).

www.elsolucionario.net

9.4 Relación entre las superclases y las subclases

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95

public void establecerApellidoPaterno( String apellido ) { apellidoPaterno = apellido; } // fin del método establecerApellidoPaterno // devuelve el apellido paterno public String obtenerApellidoPaterno() { return apellidoPaterno; } // fin del método obtenerApellidoPaterno // establece el número de seguro social public void establecerNumeroSeguroSocial( String nss ) { numeroSeguroSocial = nss; // debe validar } // fin del método establecerNumeroSeguroSocial // devuelve el número de seguro social public String obtenerNumeroSeguroSocial() { return numeroSeguroSocial; } // fin del método obtenerNumeroSeguroSocial // establece el monto de ventas brutas public void establecerVentasBrutas( double ventas ) { ventasBrutas = ( ventas < 0.0 ) ? 0.0 : ventas; } // fin del método establecerVentasBrutas // devuelve el monto de ventas brutas public double obtenerVentasBrutas() { return ventasBrutas; } // fin del método obtenerVentasBrutas // establece la tarifa de comisión public void establecerTarifaComision( double tarifa ) { tarifaComision = ( tarifa > 0.0 && tarifa < 1.0 ) ? tarifa : 0.0; } // fin del método establecerTarifaComision // devuelve la tarifa de comisión public double obtenerTarifaComision() { return tarifaComision; } // fin del método obtenerTarifaComision // calcula los ingresos public double ingresos() { return tarifaComision * ventasBrutas; } // fin del método ingresos // devuelve representación String del objeto EmpleadoPorComision2 public String toString() { return String.format( “%s: %s %s\n%s: %s\n%s: %.2f\n%s: %.2f”, “empleado por comision”, primerNombre, apellidoPaterno, “numero de seguro social”, numeroSeguroSocial,

Figura 9.9 |

EmpleadoPorComision2

con variables de instancia protected. (Parte 2 de 3).

www.elsolucionario.net

395

396

96 97 98 99

Capítulo 9

Programación orientada a objetos: herencia

“ventas brutas”, ventasBrutas, “tarifa de comision”, tarifaComision ); } // fin del método toString } // fin de la clase EmpleadoPorComision2

Figura 9.9 |

EmpleadoPorComision2

con variables de instancia protected. (Parte 3 de 3).

Podríamos haber declarado las variables de instancia primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas y tarifaComision de la superclase EmpleadoPorComision2 como public, para permitir que la subclase EmpleadoBaseMasComision2 pueda acceder a las variables de instancia de la superclase. No obstante, declarar variables de instancia public es una mala ingeniería de software, ya que permite el acceso sin restricciones a las variables de instancia, lo cual incrementa considerablemente la probabilidad de errores. Con las variables de instancia protected, la subclase obtiene acceso a las variables de instancia, pero las clases que no son subclases y las clases que no están en el mismo paquete no pueden acceder a estas variables en forma directa; recuerde que los miembros de clase protected son también visibles para las otras clases en el mismo paquete. La clase EmpleadoBaseMasComision3 (figura 9.10) es una modificación de la clase EmpleadoBaseMasComision2 (figura 9.8), que extiende a EmpleadoPorComision2 (línea 5) en vez de la clase EmpleadoPorComision. Los objetos de la clase EmpleadoBaseMasComision3 heredan las variables de instancia protected primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas y tarifaComision de EmpleadoPorComision2; ahora todas estas variables son miembros protected de EmpleadoBaseMasComision3. Como resultado, el compilador no genera errores al compilar la línea 32 del método ingresos y las líneas 40 a 42 del método toString. Si otra clase extiende a EmpleadoBasePorComision3, la nueva subclase también hereda los miembros protected.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

// Fig. 9.10: EmpleadoBaseMasComision3.java // EmpleadoBaseMasComision3 hereda de EmpleadoPorComision2 y tiene // acceso a los miembros protected de EmpleadoPorComision2. public class EmpleadoBaseMasComision3 extends EmpleadoPorComision2 { private double salarioBase; // salario base por semana // constructor con seis argumentos public EmpleadoBaseMasComision3( String nombre, String apellido, String nss, double ventas, double tarifa, double salario ) { super( nombre, apellido, nss, ventas, tarifa ); establecerSalarioBase( salario ); // valida y almacena el salario base } // fin del constructor de EmpleadoBaseMasComision3 con seis argumentos // establece el salario base public void establecerSalarioBase( double salario ) { salarioBase = ( salario < 0.0 ) ? 0.0 : salario; } // fin del método establecerSalarioBase // devuelve el salario base public double obtenerSalarioBase() { return salarioBase; } // fin del método obtenerSalarioBase

Figura 9.10 |

EmpleadoBaseMasComision3

hereda las variables de instancia protected de EmpleadoPorComision3.

(Parte 1 de 2).

www.elsolucionario.net

9.4 Relación entre las superclases y las subclases

29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

397

// calcula los ingresos public double ingresos() { return salarioBase + ( tarifaComision * ventasBrutas ); } // fin del método ingresos // devuelve representación String de EmpleadoBaseMasComision3 public String toString() { return String.format( “%s: %s %s\n%s: %s\n%s: %.2f\n%s: %.2f\n%s: %.2f”, “empleado por comision con salario base”, primerNombre, apellidoPaterno, “numero de seguro social”, numeroSeguroSocial, “ventas brutas”, ventasBrutas, “tarifa comision”, tarifaComision, “salario base”, salarioBase ); } // fin del método toString } // fin de la clase EmpleadoBaseMasComision3

Figura 9.10 |

EmpleadoBaseMasComision3

hereda las variables de instancia protected de EmpleadoPorComision3.

(Parte 2 de 2).

La clase EmpleadoBaseMasComision3 no hereda el constructor de la clase EmpleadoPorComision2. No obstante, el constructor de la clase EmpleadoBaseMasComision3 con seis argumentos (líneas 10 a 15) llama al constructor de la clase EmpleadoPorComision2 con cinco argumentos en forma explícita. El constructor de EmpleadoBaseMasComision3 con seis argumentos debe llamar en forma explícita al constructor de la clase EmpleadoPorComision2 con cinco argumentos, ya que EmpleadoPorComision2 no proporciona un constructor sin argumentos que pueda invocarse en forma implícita. La figura 9.11 utiliza un objeto EmpleadoBaseMasComision3 para realizar las mismas tareas que realizó la figura 9.7 con un objeto EmpleadoBaseMasComision (figura 9.6). Observe que los resultados de los dos programas son idénticos. Aunque declaramos la clase EmpleadoBaseMasComision sin utilizar la herencia, y declaramos la clase EmpleadoBaseMasComision3 utilizando la herencia, ambas clases proporcionan la misma funcionalidad. El código fuente para la clase EmpleadoBaseMasComision3, que tiene 45 líneas, es mucho más corto que el de la clase EmpleadoBaseMasComision, que tiene 115 líneas, debido a que la clase EmpleadoBaseMasComision3 hereda la mayor parte de su funcionalidad de EmpleadoPorComision2, mientas que la clase EmpleadoBaseMasComision sólo hereda la funcionalidad de la clase Object. Además, ahora sólo hay una copia de la funcionalidad del empleado por comisión declarada en la clase EmpleadoPorComision2. Esto hace que el código sea más fácil de mantener, modificar y depurar, ya que el código relacionado con un empleado por comisión sólo existe en la clase EmpleadoPorComision2.

1 2 3 4 5 6 7 8 9 10 11 12 13

// Fig. 9.11: PruebaEmpleadoBaseMasComision3.java // Prueba de la clase EmpleadoBaseMasComision3. public class PruebaEmpleadoBaseMasComision3 { public static void main( String args[] ) { // crea instancia de un objeto EmpleadoBaseMasComision3 EmpleadoBaseMasComision3 empleado = new EmpleadoBaseMasComision3( “Bob”, “Lewis”, “333-33-3333”, 5000, .04, 300 ); // obtiene datos del empleado por comision con sueldo base

Figura 9.11 | Miembros protected de la superclase, heredados en la subclase EmpleadoBaseMasComision3. (Parte 1 de 2).

www.elsolucionario.net

398

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

Capítulo 9

Programación orientada a objetos: herencia

System.out.println( “Informacion del empleado, obtenida por los metodos establecer: \n” ); System.out.printf( “%s %s\n”, “El primer nombre es”, empleadoBaseMasComision.obtenerPrimerNombre() ); System.out.printf( “%s %s\n”, “El apellido es”, empleadoBaseMasComision.obtenerApellidoPaterno() ); System.out.printf( “%s %s\n”, “El numero de seguro social es”, empleadoBaseMasComision.obtenerNumeroSeguroSocial() ); System.out.printf( “%s %.2f\n”, “Las ventas brutas son”, empleadoBaseMasComision.obtenerVentasBrutas() ); System.out.printf( “%s %.2f\n”, “La tarifa de comision es”, empleadoBaseMasComision.obtenerTarifaComision() ); System.out.printf( “%s %.2f\n”, “El salario base es”, empleadoBaseMasComision.obtenerSalarioBase() ); empleadoBaseMasComision.establecerSalarioBase( 1000 ); // establece el salario base System.out.printf( “\n%s:\n\n%s\n”, “Informacion actualizada del empleado, obtenida por toString”, empleadoBaseMasComision.toString() ); } // fin de main } // fin de la clase PruebaEmpleadoBaseMasComision3

Informacion del empleado, obtenida por los metodos establecer: El primer nombre es Bob El apellido es Lewis El numero de seguro social es 333-33-3333 Las ventas brutas son 5000.00 La tarifa de comision es 0.04 El salario base es 300.00 Informacion actualizada del empleado, obtenida por toString: empleado por comision con salario base: Bob Lewis numero de seguro social: 333-33-3333 ventas brutas: 5000.00 tarifa comision: 0.04 salario base: 1000.00

Figura 9.11 | Miembros protected de la superclase, heredados en la subclase EmpleadoBaseMasComision3. (Parte 2 de 2).

En este ejemplo declaramos las variables de instancia de la superclase como protected, para que las subclases pudieran heredarlas. Al heredar variables de instancia protected se incrementa un poco el rendimiento, ya que podemos acceder directamente a las variables en la subclase, sin incurrir en la sobrecarga de una llamada a un método establecer u obtener. No obstante, en la mayoría de los casos es mejor utilizar variables de instancia private, para cumplir con la ingeniería de software apropiada, y dejar al compilador las cuestiones relacionadas con la optimización de código. Su código será más fácil de mantener, modificar y depurar. El uso de variables de instancia protected crea varios problemas potenciales. En primer lugar, el objeto de la subclase puede establecer el valor de una variable heredada directamente, sin utilizar un método establecer. Por lo tanto, un objeto de la subclase puede asignar un valor inválido a la variable, con lo cual el objeto queda en un estado inconsistente. Por ejemplo, si declaramos la variable de instancia ventasBrutas de EmpleadoPorComision3 como protected, un objeto de una subclase (por ejemplo, EmpleadoBaseMasComision) podría entonces asignar un valor negativo a ventasBrutas. El segundo problema con el uso de variables de instancia protected es que hay más probabilidad de que los métodos de la subclase se escriban de manera que dependan de la implementación de datos de la superclase. En la práctica, las subclases sólo deben depender de los servicios de la superclase

www.elsolucionario.net

9.4 Relación entre las superclases y las subclases

399

(es decir, métodos que no sean private) y no en la implementación de datos de la superclase. Si hay variables de instancia protected en la superclase, tal vez necesitemos modificar todas las subclases de esa superclase si cambia la implementación de ésta. Por ejemplo, si por alguna razón tuviéramos que cambiar los nombres de las variables de instancia primerNombre y apellidoPaterno por nombre y apellido, entonces tendríamos que hacerlo para todas las ocurrencias en las que una subclase haga referencia directa a las variables de instancia primerNombre y apellidoPaterno de la superclase. En tal caso, se dice que el software es frágil o quebradizo, ya que un pequeño cambio en la superclase puede “quebrar” la implementación de la subclase. Es conveniente que el programador pueda modificar la implementación de la superclase sin dejar de proporcionar los mismos servicios a las subclases. (Desde luego que, si cambian los servicios de la superclase, debemos reimplementar nuestras subclases). Un tercer problema es que los miembros protected de una clase son visibles para todas las clases que se encuentren en el mismo paquete que la clase que contiene los miembros protected; esto no siempre es conveniente.

Observación de ingeniería de software 9.6 Use el modificador de acceso protected cuando una superclase deba proporcionar un método sólo a sus subclases y a otras clases en el mismo paquete, pero no a otros clientes.

Observación de ingeniería de software 9.7 Al declarar variables de instancia private (a diferencia de protected) en la superclase, se permite que la implementación de la superclase para estas variables de instancia cambie sin afectar las implementaciones de las subclases.

Tip para prevenir errores 9.1 Siempre que sea posible, no incluya variables de instancia protected en una superclase. En vez de ello, incluya métodos no private que accedan a las variables de instancia private. Esto asegurará que los objetos de la clase mantengan estados consistentes.

9.4.5 La jerarquía de herencia EmpleadoPorComisionEmpleadoBaseMasComision mediante el uso de variables de instancia private Ahora reexaminaremos nuestra jerarquía una vez más, pero ahora utilizaremos las mejores prácticas de ingeniería de software. La clase EmpleadoPorComision3 (figura 9.12) declara las variables de instancia primerNombre, apellidoPaterno, numeroSeguroSocial, ventasBrutas y tarifaComision como private (líneas 6 a 10) y proporciona los métodos public establecerPrimerNombre, obtenerPrimerNombre, establecerApellidoPaterno, obtenerApellidoPaterno, establecerNumeroSeguroSocial, obtenerNumeroSeguroSocial, establecerVentasBrutas, obtenerVentasBrutas, establecerTarifaComision, obtenerTarifaComision, ingresos y toString para manipular estos valores. Observe que los métodos ingresos (líneas 85 a 88) y toString (líneas 91 a 98) utilizan los métodos obtener de la clase para obtener los valores de sus variables de instancia. Si decidimos modificar los nombres de las variables de instancia, no habrá que modificar las declaraciones de ingresos y de toString; sólo habrá que modificar los cuerpos de los métodos obtener y establecer que manipulan directamente estas variables de instancia. Observe que estos cambios ocurren sólo dentro de la superclase; no se necesitan cambios en la subclase. La localización de los efectos de los cambios como éste es una buena práctica de ingeniería de software. La subclase EmpleadoBaseMasComision4 (figura 9.13) hereda los miembros no private de EmpleadoPorComision3 y puede acceder a los miembros private de su superclase, a través de esos métodos. La clase EmpleadoBaseMasComision4 (figura 9.13) tiene varios cambios en las implementaciones de sus métodos, que la diferencian de la clase EmpleadoBaseMasComision3 (figura 9.10). Los métodos ingresos (figura 9.13, líneas 31 a 34) y toString (líneas 37 a 41) invocan cada uno al método obtenerSalarioBase para obtener el valor del salario base, en vez de acceder en forma directa a salarioBase. Si decidimos cambiar el nombre de la variable de instancia salarioBase, sólo habrá que modificar los cuerpos de los métodos establecerSalarioBase y obtenerSalarioBase. El método ingresos de la clase EmpleadoBaseMasComision4 (figura 9.13, líneas 31 a 34) redefine al método ingresos de la clase EmpleadoPorComision3 (figura 9.12, líneas 85 a 88)para calcular los ingresos de un

www.elsolucionario.net

400

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58

Capítulo 9

Programación orientada a objetos: herencia

// Fig. 9.12: EmpleadoPorComision3.java // La clase EmpleadoPorComision3 representa a un empleado por comisión. public class EmpleadoPorComision3 { private String primerNombre; private String apellidoPaterno; private String numeroSeguroSocial; private double ventasBrutas; // ventas totales por semana private double tarifaComision; // porcentaje de comisión // constructor con cinco argumentos public EmpleadoPorComision3( String nombre, String apellido, String nss, double ventas, double tarifa ) { // la llamada implícita al constructor de Object ocurre aquí primerNombre = nombre; apellidoPaterno = apellido; numeroSeguroSocial = nss; establecerVentasBrutas( ventas ); // valida y almacena las ventas brutas establecerTarifaComision( tarifa ); // valida y almacena la tarifa de comisión } // fin del constructor de EmpleadoPorComision3 con cinco argumentos // establece el primer nombre public void establecerPrimerNombre( String nombre ) { primerNombre = nombre; } // fin del método establecerPrimerNombre // devuelve el primer nombre public String obtenerPrimerNombre() { return primerNombre; } // fin del método obtenerPrimerNombre // establece el apellido paterno public void establecerApellidoPaterno( String apellido ) { apellidoPaterno = apellido; } // fin del método establecerApellidoPaterno // devuelve el apellido paterno public String obtenerApellidoPaterno() { return apellidoPaterno; } // fin del método obtenerApellidoPaterno // establece el número de seguro social public void establecerNumeroSeguroSocial( String nss ) { numeroSeguroSocial = nss; // debe validar } // fin del método establecerNumeroSeguroSocial // devuelve el número de seguro social public String obtenerNumeroSeguroSocial() { return numeroSeguroSocial; } // fin del método obtenerNumeroSeguroSocial

Figura 9.12 | La clase EmpleadoPorComision3 utiliza métodos para manipular sus variables de instancia private. (Parte 1 de 2).

www.elsolucionario.net

9.4 Relación entre las superclases y las subclases

59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99

401

// establece el monto de ventas brutas public void establecerVentasBrutas( double ventas ) { ventasBrutas = ( ventas < 0.0 ) ? 0.0 : ventas; } // fin del método establecerVentasBrutas // devuelve el monto de ventas brutas public double obtenerVentasBrutas() { return ventasBrutas; } // fin del método obtenerVentasBrutas // establece la tarifa de comisión public void establecerTarifaComision( double tarifa ) { tarifaComision = ( tarifa > 0.0 && tarifa < 1.0 ) ? tarifa : 0.0; } // fin del método establecerTarifaComision // devuelve la tarifa de comisión public double obtenerTarifaComision() { return tarifaComision; } // fin del método obtenerTarifaComision // calcula los ingresos public double ingresos() { return obtenerTarifaComision() * obtenerVentasBrutas(); } // fin del método ingresos // devuelve representación String del objeto EmpleadoPorComision3 public String toString() { return String.format( “%s: %s %s\n%s: %s\n%s: %.2f\n%s: %.2f”, “empleado por comision”, obtenerPrimerNombre(), obtenerApellidoPaterno(), “numero de seguro social”, obtenerNumeroSeguroSocial(), “ventas brutas”, obtenerVentasBrutas(), “tarifa de comision”, obtenerTarifaComision() ); } // fin del método toString } // fin de la clase EmpleadoPorComision3

Figura 9.12 | La clase EmpleadoPorComision3 utiliza métodos para manipular sus variables de instancia private. (Parte 2 de 2).

empleado por comisión con sueldo base. La nueva versión obtiene la porción de los ingresos del empleado, con base en la comisión solamente, mediante una llamada al método ingresos de EmpleadoPorComision3 con la expresión super.ingresos() (figura 9.13, línea 33). El método ingresos de EmpleadoBasePorComision4 suma después el salario base a este valor, para calcular los ingresos totales del empleado. Observe la sintaxis utilizada para invocar un método sobrescrito de la superclase desde una subclase: coloque la palabra clave super y un separador punto (.) antes del nombre del método de la superclase. Esta forma de invocar métodos es una buena práctica de ingeniería de software: en la Observación de ingeniería de software 8.5 vimos que si un método realiza todas o algunas de las acciones que necesita otro método, se hace una llamada a ese método en vez de duplicar su código. Al hacer que el método ingresos de EmpleadoBaseMasComision4 invoque al método ingresos de EmpleadoPorComision3 para calcular parte de los ingresos del objeto EmpleadoBaseMasComision4, evitamos duplicar el código y se reducen los problemas de mantenimiento del mismo.

www.elsolucionario.net

402

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

// // // //

Capítulo 9

Programación orientada a objetos: herencia

Fig. 9.13: EmpleadoBaseMasComision4.java La clase EmpleadoBaseMasComision4 hereda de EmpleadoPorComision3 y accede a los datos private de EmpleadoPorComision3 a través de los métodos public de EmpleadoPorComision3.

public class EmpleadoBaseMasComision4 extends EmpleadoPorComision3 { private double salarioBase; // salario base por semana // constructor con seis argumentos public EmpleadoBaseMasComision4( String nombre, String apellido, String nss, double ventas, double tarifa, double salario ) { super( nombre, apellido, nss, ventas, tarifa ); establecerSalarioBase( salario ); // valida y almacena el salario base } // fin del constructor de EmpleadoBaseMasComision4 con seis argumentos // establece el salario base public void establecerSalarioBase( double salario ) { salarioBase = ( salario < 0.0 ) ? 0.0 : salario; } // fin del método establecerSalarioBase // devuelve el salario base public double obtenerSalarioBase() { return salarioBase; } // fin del método obtenerSalarioBase // calcula los ingresos public double ingresos() { return obtenerSalarioBase() + super.ingresos(); } // fin del método ingresos // devuelve representación String de EmpleadoBaseMasComision4 public String toString() { return String.format( “%s %s\n%s: %.2f”, “con sueldo base”, super.toString(), “sueldo base”, obtenerSalarioBase() ); } // fin del método toString } // fin de la clase EmpleadoBaseMasComision4

Figura 9.13 | La clase EmpleadoBaseMasComision4 extiende a EmpleadoPorComision3, la cual sólo proporciona variables de instancia private.

Error común de programación 9.3 Cuando se sobrescribe un método de la superclase en una subclase, por lo general, la versión correspondiente a la subclase llama a la versión de la superclase para que realice una parte del trabajo. Si no se antepone al nombre del método de la superclase la palabra clave super y el separador punto (.) cuando se hace referencia al método de la superclase, el método de la subclase se llama a sí mismo, creando potencialmente un error conocido como recursividad infinita. La recursividad, si se utiliza en forma correcta, es una poderosa herramienta, como veremos en el capítulo 15, Recursividad.

De manera similar, el método toString de EmpleadoBaseMasComision4 (figura 9.13, líneas 37 a 41) sobrescribe el método toString de la clase EmpleadoPorComision3 (figura 9.12, líneas 91 a 98) para devolver una representación String apropiada para un empleado por comisión con salario base. La nueva versión crea

www.elsolucionario.net

9.4 Relación entre las superclases y las subclases

403

parte de la representación String de un objeto EmpleadoBaseMasComision4 (es decir, la cadena "empleado por comision" y los valores de las variables de instancia private de la clase EmpleadoPorComision3), mediante una llamada al método toString de EmpleadoPorComision3 con la expresión super.toString() (figura 9.13, línea 40). Después, el método toString de EmpleadoBaseMasComision4 imprime en pantalla el resto de la representación String de un objeto EmpleadoBaseMasComision4 (es decir, el valor del salario base de la clase EmpleadoBaseMasComision4). La figura 9.14 realiza las mismas manipulaciones sobre un objeto EmpleadoBaseMasComision4 que las de las figuras 9.7 y 9.11 sobre objetos de las clases EmpleadoBaseMasComision y EmpleadoBaseMasComision3, respectivamente. Aunque cada clase de “empleado por comisión con salario base” se comporta en forma idéntica, la clase EmpleadoBaseMasComision4 es la mejor diseñada. Mediante el uso de la herencia y las llamadas a métodos que ocultan los datos y aseguran la consistencia, hemos construido una clase bien diseñada con eficiencia y efectividad. En esta sección vio la evolución de un conjunto de ejemplos diseñados cuidadosamente para enseñar las capacidades clave de la buena ingeniería de software mediante el uso de la herencia. Aprendió a usar la palabra clave extends para crear una subclase mediante la herencia, a utilizar miembros protected de la superclase para permitir que una subclase acceda a las variables de instancia heredadas de la superclase, y cómo sobrescribir los métodos de la superclase para proporcionar versiones más apropiadas para los objetos de la subclase. Además, aprendió a aplicar las técnicas de ingeniería de software del capítulo 8 y de éste, para crear clases que sean fáciles de mantener, modificar y depurar.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

// Fig. 9.14: PruebaEmpleadoBaseMasComision4.java // Prueba de la clase EmpleadoBaseMasComision4. public class PruebaEmpleadoBaseMasComision4 { public static void main( String args[] ) { // crea instancia de un objeto EmpleadoBaseMasComision4 EmpleadoBaseMasComision4 empleado = new EmpleadoBaseMasComision4( "Bob", "Lewis", "333-33-3333", 5000, .04, 300 ); // obtiene datos del empleado por comisión con sueldo base System.out.println( "Informacion del empleado obtenida por los metodos establecer: \n" ); System.out.printf( "%s %s\n", "El primer nombre es", empleado.obtenerPrimerNombre() ); System.out.printf( "%s %s\n", "El apellido es", empleado.obtenerApellidoPaterno() ); System.out.printf( "%s %s\n", "El numero de seguro social es", empleado.obtenerNumeroSeguroSocial() ); System.out.printf( "%s %.2f\n", "Las ventas brutas son", empleado.obtenerVentasBrutas() ); System.out.printf( "%s %.2f\n", "La tarifa de comision es", empleado.obtenerTarifaComision() ); System.out.printf( "%s %.2f\n", "El salario base es", empleado.obtenerSalarioBase() ); empleado.establecerSalarioBase( 1000 ); // establece el salario base System.out.printf( "\n%s:\n\n%s\n", "Informacion actualizada del empleado, obtenida por toString", empleado.toString() );

Figura 9.14 | Las variables de instancia private de la superclase son accesibles para una subclase, a través de los métodos public o protected que hereda la subclase. (Parte 1 de 2).

www.elsolucionario.net

404

34 35

Capítulo 9

Programación orientada a objetos: herencia

} // fin de main } // fin de la clase PruebaEmpleadoBaseMasComision4

Informacion del empleado obtenida por los metodos establecer: El primer nombre es Bob El apellido es Lewis El numero de seguro social es 333-33-3333 Las ventas brutas son 5000.00 La tarifa de comision es 0.04 El salario base es 300.00 Informacion actualizada del empleado, obtenida por toString: con sueldo base empleado por comision: Bob Lewis numero de seguro social: 333-33-3333 ventas brutas: 5000.00 tarifa de comision: 0.04 sueldo base: 1000.00

Figura 9.14 | Las variables de instancia private de la superclase son accesibles para una subclase, a través de los métodos public o protected que hereda la subclase. (Parte 2 de 2).

9.5 Los constructores en las subclases Como explicamos en la sección anterior, al crear una instancia de un objeto de una subclase se empieza una cadena de llamadas a los constructores, en los que el constructor de la subclase, antes de realizar sus propias tareas, invoca al constructor de su superclase, ya sea en forma explícita (por medio de la referencia super) o implícita (llamando al constructor predeterminado o sin argumentos de la superclase). De manera similar, si la superclase se deriva de otra clase (como sucede con cualquier clase, excepto Object), el constructor de la superclase invoca al constructor de la siguiente clase que se encuentre a un nivel más arriba en la jerarquía, y así en lo sucesivo. El último constructor que se llama en la cadena es siempre el de la clase Object. El cuerpo del constructor de la subclase original termina de ejecutarse al último. El constructor de cada superclase manipula las variables de instancia de la superclase que hereda el objeto de la subclase. Por ejemplo, considere de nuevo la jerarquía EmpleadoPorComision3—EmpleadoBaseMasComision4 de las figuras 9.12 y 9.13. Cuando un programa crea un objeto EmpleadoBaseMasComision4, se hace una llamada al constructor de EmpleadoBaseMasComision4. Ese constructor llama al constructor de la clase EmpleadoPorComision3, que a su vez llama en forma implícita al constructor de Object. El constructor de la clase Object tiene un cuerpo vacío, por lo que devuelve de inmediato el control al constructor de EmpleadoPorComision3, el cual inicializa las variables de instancia private de EmpleadoPorComision3 que son parte del objeto EmpleadoBaseMasComision4. Cuando este constructor termina de ejecutarse, devuelve el control al constructor de EmpleadoBaseMasComision4, el cual inicializa el salarioBase del objeto EmpleadoBaseMasComision4.

Observación de ingeniería de software 9.8 Cuando un programa crea un objeto de una subclase, el constructor de la subclase llama de inmediato al constructor de la superclase (ya sea en forma explícita, mediante super, o implícita). El cuerpo del constructor de la superclase se ejecuta para inicializar las variables de instancia de la superclase que forman parte del objeto de la subclase, después se ejecuta el cuerpo del constructor de la subclase para inicializar las variables de instancia que son parte sólo de la subclase. Java asegura que, aún si un constructor no asigna un valor a una variable de instancia, la variable de todas formas se inicializa con su valor predeterminado (es decir, 0 para los tipos numéricos primitivos, false para los tipos boolean y null para las referencias).

En nuestro siguiente ejemplo volvemos a utilizar la jerarquía de empleado por comisión, al declarar las clases (figura 9.15) y EmpladoBaseMasComision5 (figura 9.16). El constructor de cada clase imprime un mensaje cuando se le invoca, lo cual nos permite observar el orden en el que se ejecutan los constructores en la jerarquía.

EmpleadoPorComision4

www.elsolucionario.net

9.5

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

Los constructores en las subclases

405

// Fig. 9.15: EmpleadoPorComision4.java // La clase EmpleadoPorComision4 representa a un empleado por comisión. public class EmpleadoPorComision4 { private String primerNombre; private String apellidoPaterno; private String numeroSeguroSocial; private double ventasBrutas; // ventas totales por semana private double tarifaComision; // porcentaje de comisión // constructor con cinco argumentos public EmpleadoPorComision4( String nombre, String apellido, String nss, double ventas, double tarifa ) { // la llamada implícita al constructor de Object ocurre aquí primerNombre = nombre; apellidoPaterno = apellido; numeroSeguroSocial = nss; establecerVentasBrutas( ventas ); // valida y almacena las ventas brutas establecerTarifaComision( tarifa ); // valida y almacena la tarifa de comisión System.out.printf( "\nConstructor de EmpleadoPorComision4:\n%s\n", this ); } // fin del constructor de EmpleadoPorComision4 con cinco argumentos // establece el primer nombre public void establecerPrimerNombre( String nombre ) { primerNombre = nombre; } // fin del método establecerPrimerNombre // devuelve el primer nombre public String obtenerPrimerNombre() { return primerNombre; } // fin del método obtenerPrimerNombre // establece el apellido paterno public void establecerApellidoPaterno( String apellido ) { apellidoPaterno = apellido; } // fin del método establecerApellidoPaterno // devuelve el apellido paterno public String obtenerApellidoPaterno() { return apellidoPaterno; } // fin del método obtenerApellidoPaterno // establece el número de seguro social public void establecerNumeroSeguroSocial( String nss ) { numeroSeguroSocial = nss; // debe validar } // fin del método establecerNumeroSeguroSocial // devuelve el número de seguro social public String obtenerNumeroSeguroSocial() {

Figura 9.15 | El constructor de EmpleadoPorComision4 imprime texto en pantalla. (Parte 1 de 2).

www.elsolucionario.net

406

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102

Capítulo 9

Programación orientada a objetos: herencia

return numeroSeguroSocial; } // fin del método obtenerNumeroSeguroSocial // establece el monto de ventas brutas public void establecerVentasBrutas( double ventas ) { ventasBrutas = ( ventas < 0.0 ) ? 0.0 : ventas; } // fin del método establecerVentasBrutas // devuelve el monto de ventas brutas public double obtenerVentasBrutas() { return ventasBrutas; } // fin del método obtenerVentasBrutas // establece la tarifa de comisión public void establecerTarifaComision( double tarifa ) { tarifaComision = ( tarifa > 0.0 && tarifa < 1.0 ) ? tarifa : 0.0; } // fin del método establecerTarifaComision // devuelve la tarifa de comisión public double obtenerTarifaComision() { return tarifaComision; } // fin del método obtenerTarifaComision // calcula los ingresos public double ingresos() { return obtenerTarifaComision() * obtenerVentasBrutas(); } // fin del método ingresos // devuelve representación String del objeto EmpleadoPorComision4 public String toString() { return String.format( "%s: %s %s\n%s: %s\n%s: %.2f\n%s: %.2f", "empleado por comision", obtenerPrimerNombre(), obtenerApellidoPaterno(), "numero de seguro social", obtenerNumeroSeguroSocial(), "ventas brutas", obtenerVentasBrutas(), "tarifa de comision", obtenerTarifaComision() ); } // fin del método toString } // fin de la clase EmpleadoPorComision4

Figura 9.15 | El constructor de EmpleadoPorComision4 imprime texto en pantalla. (Parte 2 de 2).

La clase EmpleadoPorComision4 (figura 9.15) contiene las mismas características que la versión de la clase que se muestra en la figura 9.4. Modificamos el constructor (líneas 13 a 25) para imprimir texto en pantalla al momento en que se invoca. Observe que si imprimimos this en pantalla con el especificador de formato %s (líneas 23 y 24), invocamos en forma implícita al método toString del objeto que se está creando, para obtener la representación String de ese objeto. La clase EmpleadoBaseMasComision5 (figura 9.16) es casi idéntica a EmpleadoBaseMasComision4 (figura 9.13), sólo que el constructor de EmpleadoBaseMasComision5 también imprime texto cuando se invoca. Al igual que en EmpleadoPorComision4 (figura 9.15), imprimimos el valor de this en pantalla usando el especificador de formato %s (línea 16), para obtener de manera implícita la representación String del objeto. La figura 9.17 demuestra el orden en el que se llaman los constructores para los objetos de las clases que forman parte de una jerarquía de herencia. El método main empieza por crear una instancia del objeto empleado1 de la clase EmpleadoPorComision4 (líneas 8 y 9). A continuación, las líneas 12 a 14 crean una instancia del

www.elsolucionario.net

9.5

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

Los constructores en las subclases

407

// Fig. 9.16: EmpleadoBaseMasComision5.java // Declaración de la clase EmpleadoBaseMasComision5. public class EmpleadoBaseMasComision5 extends EmpleadoPorComision4 { private double salarioBase; // salario base por semana // constructor con seis argumentos public EmpleadoBaseMasComision5( String nombre, String apellido, String nss, double ventas, double tarifa, double salario ) { super( nombre, apellido, nss, ventas, tarifa ); establecerSalarioBase( salario ); // valida y almacena el salario base System.out.printf( "\nConstructor de EmpleadoBaseMasComision5:\n%s\n", this ); } // fin del constructor de EmpleadoBaseMasComision5 con seis argumentos // establece el salario base public void establecerSalarioBase( double salario ) { salarioBase = ( salario < 0.0 ) ? 0.0 : salario; } // fin del método establecerSalarioBase // devuelve el salario base public double obtenerSalarioBase() { return salarioBase; } // fin del método obtenerSalarioBase // calcula los ingresos public double ingresos() { return obtenerSalarioBase() + super.ingresos(); } // fin del método ingresos // devuelve representación String de EmpleadoBaseMasComision5 public String toString() { return String.format( "%s %s\n%s: %.2f", "con sueldo base", super.toString(), "sueldo base", obtenerSalarioBase() ); } // fin del método toString } // fin de la clase EmpleadoBaseMasComision5

Figura 9.16 | El constructor de EmpleadoBaseMasComision5 imprime texto en pantalla.

1 2 3 4 5 6 7 8 9 10 11

// Fig. 9.17: PruebaConstructores.java // Muestra el orden en el que se llaman los constructores de la superclase y la subclase. public class PruebaConstructores { public static void main( String args[] ) { EmpleadoPorComision4 empleado1 = new EmpleadoPorComision4( "Bob", "Lewis", "333-33-3333", 5000, .04 ); System.out.println();

Figura 9.17 | Orden de llamadas a los constructores. (Parte 1 de 2).

www.elsolucionario.net

408

12 13 14 15 16 17 18 19 20 21

Capítulo 9

Programación orientada a objetos: herencia

EmpleadoBaseMasComision5 empleado2 = new EmpleadoBaseMasComision5( "Lisa", "Jones", "555-55-5555", 2000, .06, 800 ); System.out.println(); EmpleadoBaseMasComision5 empleado3 = new EmpleadoBaseMasComision5( "Mark", "Sands", "888-88-8888", 8000, .15, 2000 ); } // fin de main } // fin de la clase PruebaConstructores

Constructor de EmpleadoPorComision4: empleado por comision: Bob Lewis numero de seguro social: 333-33-3333 ventas brutas: 5000.00 tarifa de comision: 0.04

Constructor de EmpleadoPorComision4: con sueldo base empleado por comision: Lisa Jones numero de seguro social: 555-55-5555 ventas brutas: 2000.00 tarifa de comision: 0.06 sueldo base: 0.00 Constructor de EmpleadoBaseMasComision5: con sueldo base empleado por comision: Lisa Jones numero de seguro social: 555-55-5555 ventas brutas: 2000.00 tarifa de comision: 0.06 sueldo base: 800.00

Constructor de EmpleadoPorComision4: con sueldo base empleado por comision: Mark Sands numero de seguro social: 888-88-8888 ventas brutas: 8000.00 tarifa de comision: 0.15 sueldo base: 0.00 Constructor de EmpleadoBaseMasComision5: con sueldo base empleado por comision: Mark Sands numero de seguro social: 888-88-8888 ventas brutas: 8000.00 tarifa de comision: 0.15 sueldo base: 2000.00

Figura 9.17 | Orden de llamadas a los constructores. (Parte 2 de 2). objeto empleado2 de EmpleadoBaseMasComision5. Esto invoca al constructor de EmpleadoPorComision4, el cual imprime los resultados con los valores que recibe del constructor de EmpleadoBaseMasComision5 y después imprime los resultados especificados en el constructor de EmpleadoBaseMasComision5. Después, las líneas 17 a 19 crean una instancia del objeto empleado3 de EmpleadoBaseMasComision5. De nuevo, se hacen llamadas a los constructores de EmpleadoPorComision4 y EmpleadoBaseMasComision5. En cada caso, el cuerpo del constructor de EmpleadoPorComision4 se ejecuta antes que el cuerpo del constructor de EmpleadoBaseMasComision5. Observe que empleado2 se construye por completo antes que empiece el constructor de empleado3.

www.elsolucionario.net

9.6

Ingeniería de software mediante la herencia

409

9.6 Ingeniería de software mediante la herencia En esta sección hablaremos sobre la personalización del software existente mediante la herencia. Cuando una nueva clase extiende a una clase existente, la nueva clase hereda los miembros no private de la clase existente. Podemos personalizar la nueva clase para cumplir nuestras necesidades, mediante la inclusión de miembros adicionales y la sobrescritura de miembros de la superclase. Para hacer esto, el programador de la subclase4 no tiene que modificar el código fuente de la superclase. Java sólo requiere el acceso al archivo .class de la superclase, para poder compilar y ejecutar cualquier programa que utilice o extienda la superclase. Esta poderosa capacidad es atractiva para los distribuidores independientes de software (ISVs), quienes pueden desarrollar clases propietarias para vender o licenciar, y ponerlas a disposición de los usuarios en formato de código de bytes. Después, los usuarios pueden derivar con rapidez nuevas clases a partir de estas clases de biblioteca, sin necesidad de acceder al código fuente propietario del ISV.

Observación de ingeniería de software 9.9 A pesar del hecho de que al heredar de una clase no se requiere acceso al código fuente de esa clase, los desarrolladores insisten con frecuencia en ver el código fuente para comprender cómo está implementada la clase. Los desarrolladores en la industria desean asegurarse que están extendiendo una clase sólida; por ejemplo, una clase que se desempeñe bien y que se implemente en forma segura.

Algunas veces, los estudiantes tienen dificultad para apreciar el alcance de los problemas a los que se enfrentan los diseñadores que trabajan en proyectos de software a gran escala en la industria. Las personas experimentadas con esos proyectos dicen que la reutilización efectiva del software mejora el proceso de desarrollo del mismo. La programación orientada a objetos facilita la reutilización de software, con lo que se obtiene una potencial reducción en el tiempo de desarrollo. La disponibilidad de bibliotecas de clases extensas y útiles produce los máximos beneficios de la reutilización de software a través de la herencia. Los diseñadores de aplicaciones crean sus aplicaciones con estas bibliotecas, y los diseñadores de bibliotecas obtienen su recompensa al incluir sus bibliotecas con las aplicaciones. Las bibliotecas de clases estándar de Java que se incluyen con Java SE 6 tienden a ser de propósito general. Existen muchas bibliotecas de clases de propósito especial, y muchas más están en proceso de crearse.

Observación de ingeniería de software 9.10 En la etapa de diseño de un sistema orientado a objetos, el diseñador encuentra comúnmente que ciertas clases están muy relacionadas. Es conveniente que el diseñador “factorice” las variables de instancia y los métodos comunes, y los coloque en una superclase. Después debe usar la herencia para desarrollar subclases, especializándolas con herramientas que estén más allá de las heredadas de parte de la superclase.

Observación de ingeniería de software 9.11 Declarar una subclase no afecta el código fuente de la superclase. La herencia preserva la integridad de la superclase.

Observación de ingeniería de software 9.12 Así como los diseñadores de sistemas no orientados a objetos deben evitar la proliferación de métodos, los diseñadores de sistemas orientados a objetos deben evitar la proliferación de clases. Dicha proliferación crea problemas administrativos y puede obstaculizar la reutilización de software, ya que en una biblioteca de clases enorme es difícil para un cliente localizar las clases más apropiadas. La alternativa es crear menos clases que proporcionen una funcionalidad más substancial, pero dichas clases podrían volverse complejas.

Tip de rendimiento 9.1 Si las subclases son más grandes de lo necesario (es decir, que contengan demasiada funcionalidad), podrían desperdiciarse los recursos de memoria y de procesamiento. Extienda la superclase que contenga la funcionalidad que esté más cerca de lo que usted necesita.

Puede ser confuso leer las declaraciones de las subclases, ya que los miembros heredados no se declaran de manera explícita en las subclases, sin embargo, están presentes en ellas. Hay un problema similar a la hora de documentar los miembros de las subclases.

www.elsolucionario.net

410

Capítulo 9

Programación orientada a objetos: herencia

9.7 La clase object

Como vimos al principio en este capítulo, todas las clases en Java heredan, ya sea en forma directa o indirecta de la clase Object (paquete java.lang), por lo que todas las demás clases heredan sus 11 métodos. La figura 9.18 muestra un resumen de los métodos de Object. A lo largo de este libro veremos varios de los métodos de Object (como se indica en la figura 9.18). Puede aprender más acerca de los métodos de Object en la documentación en línea de la API de Object, y en el tutorial de Java (The Java Tutorial) en los siguientes sitios: java.sun.com/javase/6/docs/api/java/lang/Object.html java.sun.com/docs/books/tutorial/java/IandI/objectclass.html

En el capítulo 7 vimos que los arreglos son objetos. Como resultado, al igual que otros objetos, un arreglo hereda los miembros de la clase Object. Observe que todo arreglo tiene un método clone sobrescrito, que copia el arreglo. No obstante, si el arreglo almacena referencias a objetos, los objetos no se copian. Para obtener más información acerca de la relación entre los arreglos y la clase Object, por favor consulte la Especificación del lenguaje Java, capítulo 10, en java.sun.com/docs/books/jls/second_edition/html/arrays.doc.html

Método

Descripción

clone

Este método protected, que no recibe argumentos y devuelve una referencia Object, realiza una copia del objeto en el que se llama. Cuando se requiere la clonación para los objetos de una clase, ésta debe sobrescribir el método clone como un método public, y debe implementar la interfaz Cloneable (paquete java.lang). La implementación predeterminada de este método realiza algo que se conoce como copia superficial: los valores de las variables de instancia en un objeto se copian a otro objeto del mismo tipo. Para los tipos por referencia, sólo se copian las referencias. Una implementación típica del método clone sobrescrito sería realizar una copia en profundidad, que crea un nuevo objeto para cada variable de instancia de tipo por referencia. Hay muchos detalles sutiles en cuanto a sobrescribir el método clone. Puede aprender más acerca de la clonación en el siguiente artículo: java.sun.com/developer/JDCTechTips/2001/tt0306.html

equals

Este método compara la igualdad entre dos objetos; devuelve true si son iguales y false en caso contrario. El método recibe cualquier objeto Object como argumento. Cuando debe compararse la igualdad entre objetos de una clase en particular, la clase debe sobrescribir el método equals para comparar el contenido de los dos objetos. La implementación de este método debe cumplir los siguientes requerimientos: • Debe devolver false si el argumento es null. • Debe devolver true si un objeto se compara consigo mismo, como en objeto1.equals( objeto1 ). • Debe devolver true sólo si tanto objeto1.equals( objeto2 ) como objeto2.equals( objeto1 ) devuelven true. • Para tres objetos, si objeto1.equals( objeto2 ) devuelve true y objeto2.equals( objeto3 ) devuelve true, entonces objeto1.equals( objeto3 ) también debe devolver true. • Si equals se llama varias veces con los dos objetos, y éstos no cambian, el método debe devolver true de manera consistente si los objetos son iguales, y false en caso contrario. Una clase que sobrescribe a equals también debe sobrescribir hashCode para asegurar que los objetos iguales tengan códigos de hash idénticos. La implementación equals predeterminada utiliza el operador == para determinar si dos referencias se refieren al mismo objeto en la memoria. La sección 30.3.3 demuestra el método equals de la clase String y explica la diferencia entre comparar objetos String con == y con equals.

Figura 9.18 | Los métodos de Object que todas las clases heredan en forma directa o indirecta. (Parte 1 de 2).

www.elsolucionario.net

9.8

(Opcional) Ejemplo práctico de GUI y gráficos: mostar texto e imágenes usando etiquetas

411

Método

Descripción

finalize

El recolector de basura llama a este método protected (presentado en las secciones 8.10 y 8.11) para realizar las tareas de preparación para la terminación en un objeto, justo antes de que el recolector de basura reclame la memoria de ese objeto. No se garantiza que el recolector de basura vaya a reclamar un objeto, por lo que no se puede garantizar que se ejecute el método finalize del objeto. El método debe especificar una lista de parámetros vacía y debe devolver void. La implementación predeterminada de este método sirve como un receptáculo que no hace nada.

getClass

Todo objeto en Java conoce su tipo en tiempo de ejecución. El método getClass (utilizado en las secciones 10.5 y 21.3) devuelve un objeto de la clase Class (paquete java.lang), el cual contiene información acerca del tipo del objeto, como el nombre de su clase (devuelto por el método getName de Class). Puede aprender más acerca de la clase Class en la documentación de la API en línea, en java. sun.com/javase/6/docs/api/java/lang/Class.html.

hashCode

Una tabla de hash es una estructura de datos (descrita en la sección 19.10) que relaciona a un objeto, llamado la clave, con otro objeto, llamado el valor. Cuando inicialmente se inserta un valor en una tabla de hash, se hace una llamada al método hashCode de la clave. La tabla de hash utiliza el valor de código de hash devuelto para determinar la ubicación en la que se debe insertar el valor correspondiente. La tabla de hash también utiliza el código de hash de la clave para localizar el valor correspondiente de la misma.

notify, notifyAll, wait

Los métodos notify, notifyAll y las tres versiones sobrecargadas de wait están relacionados con el subprocesamiento múltiple, que veremos en el capítulo 23. En versiones recientes de Java, el modelo de subprocesamiento múltiple ha cambiado en forma considerable, pero estas características se siguen soportando.

toString

Este método (presentado en la sección 9.4.1) devuelve una representación String de un objeto. La implementación predeterminada de este método devuelve el nombre del paquete y el nombre de la clase del objeto, seguidos por una representación hexadecimal del valor devuelto por el método hashCode del objeto.

Figura 9.18 | Los métodos de Object que todas las clases heredan en forma directa o indirecta. (Parte 2 de 2).

9.8 (Opcional) Ejemplo práctico de GUI y gráficos: mostar texto e imágenes usando etiquetas A menudo, los programas usan etiquetas cuando necesitan mostrar información o instrucciones al usuario, en una interfaz gráfica de usuario. Las etiquetas son una forma conveniente de identificar componentes de la GUI en la pantalla, y de mantener al usuario informado acerca del estado actual del programa. En Java, un objeto de la clase JLabel (del paquete javax.swing) puede mostrar una sola línea de texto, una imagen o ambos. El ejemplo de la figura 9.19 demuestra varias características de JLabel. Las líneas 3 a 6 importan las clases que necesitamos para mostrar los objetos JLabel.BorderLayout del paquete java.awt contienen constantes que especifican en dónde podemos colocar componentes de GUI en el objeto JFrame. La clase ImageIcon representa una imagen que puede mostrarse en un JLabel, y la clase JFrame representa la ventana que contiene todas las etiquetas. La línea 13 crea un objeto JLabel que muestra el argumento de su constructor: la cadena "Norte". La línea 16 declara la variable local etiquetaIcono y le asigna un nuevo objeto ImageIcon. El constructor para ImageIcon recibe un objeto String que especifica la ruta del archivo de la imagen. Como sólo especificamos un nombre de archivo, Java supone que se encuentra en el mismo directorio que la clase DemoLabel. ImageIcon puede cargar imágenes en los formatos GIF, JPEG y PNG. La línea 19 declara e inicializa la variable local etiquetaCentro con un objeto JLabel que muestra el objeto etiquetaIcono. La línea 22 declara e inicializa la variable local etiquetaSur con un objeto JLabel similar al de la línea 19. Sin embargo, la línea 25 llama al método setText para modificar el texto que muestra la etiqueta. El método setText puede llamarse en cualquier objeto JLabel para modificar su texto. Este objeto JLabel muestra tanto el icono como el texto.

www.elsolucionario.net

412

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

Capítulo 9

Programación orientada a objetos: herencia

// Fig 9.19: DemoLabel.java // Demuestra el uso de etiquetas. import java.awt.BorderLayout; import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.JFrame; public class DemoLabel { public static void main( String args[] ) { // Crea una etiqueta con texto solamente JLabel etiquetaNorte = new JLabel( "Norte" ); // crea un icono a partir de una imagen, para poder colocarla en un objeto JLabel ImageIcon etiquetaIcono = new ImageIcon( "GUItip.gif" ); // crea una etiqueta con un icono en vez de texto JLabel etiquetaCentro = new JLabel( etiquetaIcono ); // crea otra etiqueta con un icono JLabel etiquetaSur = new JLabel( etiquetaIcono ); // establece la etiqueta para mostrar texto (así como un icono) etiquetaSur.setText( "Sur" ); // crea un marco para contener las etiquetas JFrame aplicacion = new JFrame(); aplicacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); // agrega las etiquetas al marco; el segundo argumento especifica // en qué parte del marco se va a agregar la etiqueta aplicacion.add( etiquetaNorte, BorderLayout.NORTH ); aplicacion.add( etiquetaCentro, BorderLayout.CENTER ); aplicacion.add( etiquetaSur, BorderLayout.SOUTH ); aplicacion.setSize( 300, 300 ); // establece el tamaño del marco aplicacion.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase DemoLabel

Norte

Archivo Nuevo Abrir... Cerrar

Archivo Nuevo Abrir... Cerrar

Figura 9.19 |

JLabel

Sur

con texto y con imágenes.

www.elsolucionario.net

9.9

Conclusión

413

La línea 28 crea el objeto JFrame que muestra a los objetos JLabel, y la línea 30 indica que el programa debe terminar cuando se cierre el objeto JFrame. Para adjuntar las etiquetas al objeto JFrame en las líneas 34 a 36, llamamos a una versión sobrecargada del método add que recibe dos parámetros. El primer parámetro es el componente que deseamos adjuntar, y el segundo es la región en la que debe colocarse. Cada objeto JFrame tiene un esquema asociado, que ayuda al JFrame a posicionar los componentes de la GUI que tiene adjuntos. El esquema predeterminado para un objeto JFrame se conoce como BorderLayout, y tiene cinco regiones: NORTH (superior), SOUTH (inferior), EAST (lado derecho), WEST (lado izquierdo) y CENTER (centro). Cada una de estas regiones se declara como una constante en la clase BorderLayout. Al llamar al método add con un argumento, el objeto JFrame coloca el componente en la región CENTER de manera automática. Si una posición ya contiene un componente, entonces el nuevo componente toma su lugar. Las líneas 38 y 39 establecen el tamaño del objeto JFrame y lo hacen visible en pantalla.

Ejercicio del ejemplo práctico de GUI y gráficos 9.1 Modifique el ejercicio 8.1 para incluir un objeto JLabel como barra de estado, que muestre las cuentas que representan el número de cada figura mostrada. La clase PanelDibujo debe declarar un método que devuelva un objeto String que contenga el texto de estado. En main, primero cree el objeto PanelDibujo, y después cree el objeto JLabel con el texto de estado como argumento para el constructor de JLabel. Adjunte el objeto JLabel a la región SOUTH del objeto JFrame, como se muestra en la figura 9.20.

Lineas: 5, Ovalos: 4, Rectangulos: 5

Figura 9.20 | Objeto JLabel que muestra las estadísticas de las figuras.

9.9 Conclusión En este capítulo se introdujo el concepto de la herencia: la habilidad de crear clases mediante la absorción de los miembros de una clase existente, mejorándolos con nuevas capacidades. Usted aprendió las nociones de las superclases y las subclases, y utilizó la palabra clave extends para crear una subclase que hereda miembros de una superclase. En este capítulo se introdujo también el modificador de acceso protected; los métodos de la subclase pueden acceder a los miembros protected de la superclase. Aprendió también cómo acceder a los miembros de la superclase mediante super. Vio además cómo se utilizan los constructores en las jerarquías de herencia. Por último, aprendió acerca de los métodos de la clase Object, la superclase directa o indirecta de todas las clases en Java.

www.elsolucionario.net

414

Capítulo 9

Programación orientada a objetos: herencia

En el capítulo 10, Programación orientada a objetos: polimorfismo, continuaremos con nuestra discusión sobre la herencia al introducir el polimorfismo: un concepto orientado a objetos que nos permite escribir programas que puedan manipular convenientemente, de una forma más general, objetos de una amplia variedad de clases relacionadas por la herencia. Después de estudiar el capítulo 10, estará familiarizado con las clases, los objetos, el encapsulamiento, la herencia y el polimorfismo: las tecnologías clave de la programación orientada a objetos.

Resumen Sección 9.1 Introducción • La reutilización de software reduce el tiempo de desarrollo de los programas. • La superclase directa de una subclase (que se especifica mediante la palabra extends en la primera línea de una declaración de clase) es la superclase a partir de la cual hereda la subclase. Una superclase indirecta de una subclase se encuentra dos o más niveles arriba de esa subclase en la jerarquía de clases. • En la herencia simple, una clase se deriva de una superclase directa. En la herencia múltiple, una clase se deriva de más de una superclase directa. Java no soporta la herencia múltiple. • Una subclase es más específica que su superclase, y representa un grupo más pequeño de objetos. • Cada objeto de una subclase es también un objeto de la superclase de esa clase. Sin embargo, el objeto de una superclase no es un objeto de las subclases de su clase. • Una relación “es un” representa a la herencia. En una relación “es un”, un objeto de una subclase también puede tratarse como un objeto de su superclase. • Una relación “tiene un” representa a la composición. En una relación “tiene un”, el objeto de una clase contiene referencias a objetos de otras clases.

Sección 9.2 Superclases y subclases • Las relaciones de herencia simple forman estructuras jerárquicas tipo árbol; una superclase existe en una

relación jerárquica con sus subclases.

Sección 9.3 Miembros protected • Los miembros public de una superclase son accesibles en cualquier parte en donde el programa tenga una referencia a un objeto de esa superclase, o de una de sus subclases. • Los miembros private de una superclase son accesibles sólo dentro de la declaración de esa superclase. • Los miembros protected de una superclase tienen un nivel intermedio de protección entre acceso public y private. Pueden ser utilizados por los miembros de la superclase, los miembros de sus subclases y los miembros de otras clases en el mismo paquete. • Cuando un método de una subclase sobrescribe a un método de una superclase, se puede acceder al método de la superclase desde la subclase, si se antepone al nombre del método de la subclase la palabra clave super y un separador punto (.).

Sección 9.4 Relación entre las superclases y las subclases • Una subclase no puede acceder o heredar los miembros private de su superclase; al permitir esto se violaría el encapsulamiento de la superclase. Sin embargo, una subclase puede heredar los miembros no private de su superclase. • El método de una superclase puede sobrescribirse en una clase para declarar una implementación apropiada para la subclase. • El método toString no recibe argumentos y devuelve un objeto String. Por lo general, una subclase sobrescribe el método toString de la clase Object. • Cuando se imprime un objeto usando el especificador de formato %s, se hace una llamada implícita al método toString del objeto para obtener su representación de cadena.

Sección 9.5 Los constructores en las subclases • La primera tarea de cualquier constructor de subclase es llamar al constructor de su superclase directa, ya sea en forma explícita o implícita, para asegurar que las variables de instancia heredadas de la superclase se inicialicen en forma apropiada.

www.elsolucionario.net

Ejercicios de autoevaluación

415

• Una subclase puede invocar en forma explícita a un constructor de su superclase; para ello utiliza la sintaxis de llamada del constructor de la superclase: la palabra clave super, seguida de un conjunto de paréntesis que contienen los argumentos del constructor de la superclase.

Sección 9.6 Ingeniería de Software mediante la herencia • Declarar variables de instancia private, al mismo tiempo que se proporcionan métodos no private para manipular y realizar la validación, ayuda a cumplir con la buena ingeniería de software.

Terminología biblioteca de clases

método heredado miembro heredado Object, clase objeto de una subclase objeto de una superclase private, miembro de superclase protected, miembro de superclase protected, palabra clave public, miembro de superclase relación jerárquica reutilización de software sintaxis de llamada al constructor de una superclase sobrescribir (redefinir) el método de una superclase software frágil software quebradizo subclase super, palabra clave superclase superclase directa superclase indirecta tiene un, relación toString, método de la clase Object

clase base clase derivada clone, método de la clase Object componentes reutilizables estandarizados composición constructor de subclase constructor de superclase constructor de superclase sin argumentos diagrama de jerarquía equals, método de la clase Object es un, relación especialización extends, palabra clave getClass, método de la clase Object hashCode, método de la clase Object herencia herencia simple invocar al constructor de una superclase invocar al método de una superclase jerarquía de clases jerarquía de herencia

Ejercicios de autoevaluación 9.1

Complete las siguientes oraciones: a) ____________ es una forma de reutilización de software, en la que nuevas clases adquieren los miembros de las clases existentes, y se mejoran con nuevas capacidades. b) Los miembros ____________ de una superclase pueden utilizarse en la declaración de la superclase y en las declaraciones de las subclases. c) En una relación ____________, un objeto de una subclase puede ser tratado también como un objeto de su superclase. d) En una relación ____________, el objeto de una clase tiene referencias a objetos de otras clases como miembros. e) En la herencia simple, una clase existe en una relación ____________ con sus subclases. f ) Los miembros ____________ de una superclase son accesibles en cualquier parte en donde el programa tenga una referencia a un objeto de esa superclase, o a un objeto de una de sus subclases. g) Cuando se crea la instancia de un objeto de una subclase, el ____________ de una superclase se llama en forma implícita o explícita. h) Los constructores de una subclase pueden llamar a los constructores de la superclase mediante la palabra clave ____________.

9.2

Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué. a) Los constructores de la superclase no son heredados por las subclases. b) Una relación “tiene un” se implementa mediante la herencia.

www.elsolucionario.net

416

Capítulo 9

Programación orientada a objetos: herencia

c) Una clase Auto tiene una relación “es un” con las clases VolanteDireccion y Frenos. d) La herencia fomenta la reutilización de software comprobado, de alta calidad. e) Cuando una subclase redefine al método de una superclase utilizando la misma firma, se dice que la subclase sobrecarga a ese método de la superclase.

Respuestas a los ejercicios de autoevaluación 9.1 a) Herencia. b) public y protected. c) “es un” o de herencia. d) “tiene-un”, o composición. e) jerárquica. f ) public. g) constructor. h) super. 9.2 a) Verdadero. b) Falso. Una relación “tiene un” se implementa mediante la composición. Una relación “es-un” se implementa mediante la herencia. c) Falso. Éste es un ejemplo de una relación “tiene un”. La clase Auto tiene una relación “es-un” con la clase Vehiculo. d) Verdadero. e) Falso. Esto se conoce como sobrescritura, no sobrecarga; un método sobrecargado tiene el mismo nombre, pero una firma distinta.

Ejercicios 9.3 Muchos programas escritos con herencia podrían escribirse mediante la composición, y viceversa. Vuelva a escribir las clases EmpleadoBaseMasComision4 (figura 9.13) de la jerarquía EmpleadoPorComision3-EmpleadoBaseMasComision4 para usar la composición en vez de la herencia. Una vez que haga esto, valore los méritos relativos de las dos metodologías para los problemas de EmpleadoPorComision3 y EmpleadoBaseMasComision4, así como también para los programas orientados a objetos en general. ¿Cuál metodología es más natural? ¿Por qué? 9.4 Describa las formas en las que la herencia fomenta la reutilización de software, ahorra tiempo durante el desarrollo de los programas y ayuda a prevenir errores. 9.5 Dibuje una jerarquía de herencia para los estudiantes en una universidad, de manera similar a la jerarquía que se muestra en la figura 9.2. Use a Estudiante como la superclase de la jerarquía, y después extienda Estudiante con las clases EstudianteNoGraduado y EstudianteGraduado. Siga extendiendo la jerarquía con el mayor número de niveles que sea posible. Por ejemplo, EstudiantePrimerAnio, EstudianteSegundoAnio, EstudianteTercerAnio y EstudianteCuartoAnio podrían extender a EstudianteNoGraduado, y EstudianteDoctorado y EstudianteMaestria podrían ser subclases de EstudianteGraduado. Después de dibujar la jerarquía, hable sobre las relaciones que existen entre las clases. [Nota: no necesita escribir código para este ejercicio]. 9.6 El mundo de las figuras es más extenso que las figuras incluidas en la jerarquía de herencia de la figura 9.3. Anote todas las figuras en las que pueda pensar (tanto bidimensionales como tridimensionales) e intégrelas en una jerarquía Figura más completa, con todos los niveles que sea posible. Su jerarquía debe tener la clase Figura en la parte superior. Las clases FiguraBidimensional y FiguraTridimensional deben extender a Figura. Agregue subclases adicionales, como Cuadrilatero y Esfera, en sus ubicaciones correctas en la jerarquía, según sea necesario. 9.7 Algunos programadores prefieren no utilizar el acceso protected, pues piensan que quebranta el encapsulamiento de la superclase. Hable sobre los méritos relativos de utilizar el acceso protected, en comparación con el acceso private en las superclases. Escriba una jerarquía de herencia para las clases Cuadrilatero, Trapezoide, Paralelogramo, Rectangulo y Use Cuadrilatero como la superclase de la jerarquía. Agregue todos los niveles que sea posible a la jerarquía. Especifique las variables de instancia y los métodos para cada clase. Las variables de instancia private de Cuadrilatero deben ser los pares de coordenadas x-y para los cuatro puntos finales del Cuadrilatero. Escriba un programa que cree instancias de objetos de sus clases, y que imprima el área de cada objeto (excepto Cuadrilatero). 9.8

Cuadrado.

www.elsolucionario.net

10 Un anillo para gobernarlos a todos, un anillo para encontrarlos, un anillo para traerlos a todos y en la oscuridad enlazarlos.

Programación orientada a objetos: polimorfismo

—John Ronald Reuel Tolkien

Las proposiciones generales no deciden casos concretos. —Oliver Wendell Holmes

Un filósofo de imponente estatura no piensa en un vacío. Incluso sus ideas más abstractas son, en cierta medida, condicionadas por lo que se conoce o no en el tiempo en que vive. —Alfred North Whitehead

¿Por qué, alma mía, desfalleces y te agitas por mí?

OBJETIVOS En este capítulo aprenderá a: Q

Comprender el concepto de polimorfismo.

Q

Aprender a utilizar métodos sobrescritos para llevar a cabo el polimorfismo.

Q

Distinguir entre clases abstractas y concretas.

Q

Aprender a declarar métodos abstract para crear clases abstractas.

Q

Apreciar la manera en que el polimorfismo hace que los sistemas puedan extenderse y mantenerse.

Q

Determinar el tipo de un objeto en tiempo de ejecución.

Q

Aprender a declarar e implementar interfaces.

—Salmos 42:5

www.elsolucionario.net

Pla n g e ne r a l

418

Capítulo 10

10.1 10.2 10.3 10.4 10.5

10.6 10.7

10.8 10.9 10.10

Programación orientada a objetos: polimorfismo

Introducción Ejemplos del polimorfismo Demostración del comportamiento polimórfico Clases y métodos abstractos Ejemplo práctico: sistema de nómina utilizando polimorfismo 10.5.1 Creación de la superclase abstracta Empleado 10.5.2 Creación de la subclase concreta EmpleadoAsalariado 10.5.3 Creación de la subclase concreta EmpleadoPorHoras 11.5.4 Creación de la subclase concreta EmpleadoPorComision 10.5.5 Creación de la subclase concreta indirecta EmpleadoBaseMasComision 10.5.6 Demostración del procesamiento polimórfico, el operador instanceof y la conversión descendente 10.5.7 Resumen de las asignaciones permitidas entre variables de la superclase y de la subclase Métodos y clases final Ejemplo práctico: creación y uso de interfaces 10.7.1 Desarrollo de una jerarquía PorPagar 10.7.2 Declaración de la interfaz PorPagar 10.7.3 Creación de la clase Factura 10.7.4 Modificación de la clase Empleado para implementar la interfaz PorPagar 10.7.5 Modificación de la clase EmpleadoAsalariado para usarla en la jerarquía PorPagar 10.7.6 Uso de la interfaz PorPagar para procesar objetos Factura y Empleado mediante el polimorfismo 10.7.7 Declaración de constantes con interfaces 10.7.8 Interfaces comunes de la API de Java (Opcional) Ejemplo práctico de GUI y gráficos: realizar dibujos mediante el polimorfismo (Opcional) Ejemplo práctico de Ingeniería de Software: incorporación de la herencia en el sistema ATM Conclusión

Resumen | Terminología | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

10.1 Introducción Ahora continuaremos nuestro estudio de la programación orientada a objetos, explicando y demostrando el polimorfismo con las jerarquías de herencia. El polimorfismo nos permite “programar en forma general”, en vez de “programar en forma específica”. En especial, nos permite escribir programas que procesen objetos que compartan la misma superclase en una jerarquía de clases, como si todos fueran objetos de la superclase; esto puede simplificar la programación. Considere el siguiente ejemplo de polimorfismo. Suponga que crearemos un programa que simula el movimiento de varios tipos de animales para un estudio biológico. Las clases Pez, Rana y Ave representan los tres tipos de animales bajo investigación. Imagine que cada una de estas clases extiende a la superclase Animal, la cual contiene un método llamado mover y mantiene la posición actual de un animal, en forma de coordenadas x-y. Cada subclase implementa el método mover. Nuestro programa mantiene un arreglo de referencias a objetos de las diversas subclases de Animal. Para simular los movimientos de los animales, el programa envía a cada objeto el mismo mensaje una vez por segundo; a saber, mover. No obstante, cada tipo específico de Animal responde a un mensaje mover de manera única; un Pez podría nadar tres pies, una Rana podría saltar cinco pies y un Ave podría volar diez pies. El programa envía el mismo mensaje (es decir, mover) a cada objeto animal en forma genérica, pero cada objeto sabe cómo modificar sus coordenadas x-y en forma apropiada para su tipo específico de movimiento. Confiar en que cada objeto sepa cómo “hacer lo correcto” (es decir, lo que sea apropiado para ese tipo de objeto) en respuesta a la llamada al mismo método es el concepto clave del polimorfismo. El mismo mensaje

www.elsolucionario.net

10.2

Ejemplos del polimorfismo

419

(en este caso, mover) que se envía a una variedad de objetos tiene “muchas formas” de resultados; de aquí que se utilice el término polimorfismo. Con el polimorfismo podemos diseñar e implementar sistemas que puedan extenderse con facilidad; pueden agregarse nuevas clases con sólo modificar un poco (o nada) las porciones generales de la aplicación, siempre y cuando las nuevas clases sean parte de la jerarquía de herencia que la aplicación procesa en forma genérica. Las únicas partes de un programa que deben alterarse para dar cabida a las nuevas clases son las que requieren un conocimiento directo de las nuevas clases que el programador agregará a la jerarquía. Por ejemplo, si extendemos la clase Animal para crear la clase Tortuga (que podría responder a un mensaje mover caminando una pulgada), necesitamos escribir sólo la clase Tortuga y la parte de la simulación que crea una instancia de un objeto Tortuga. Las porciones de la simulación que procesan a cada Animal en forma genérica pueden permanecer iguales. Este capítulo se divide en varias partes. Primero hablaremos sobre los ejemplos comunes del polimorfismo. Después proporcionaremos un ejemplo que demuestra el comportamiento polimórfico. Utilizaremos referencias a la superclase para manipular tanto a los objetos de la superclase como a los objetos de las subclases mediante el polimorfismo. Después presentaremos un ejemplo práctico en el que utilizaremos nuevamente la jerarquía de empleados de la sección 9.4.5. Desarrollaremos una aplicación simple de nómina que calcula mediante el polimorfismo el salario semanal de varios tipos de empleados, usando el método ingresos de cada empleado. Aunque los ingresos de cada tipo de empleado se calculan de una manera específica, el polimorfismo nos permite procesar a los empleados “en general”. En el ejemplo práctico ampliaremos la jerarquía para incluir dos nuevas clases: EmpleadoAsalariado (para las personas que reciben un salario semanal fijo) y EmpleadoPorHoras (para las personas que reciben un salario por horas y “tiempo y medio” por el tiempo extra). Declararemos un conjunto común de funcionalidad para todas las clases en la jerarquía actualizada en una clase “abstracta” llamada Empleado, a partir de la cual las clases EmpleadoAsalariado, EmpleadoPorHoras y EmpleadoPorComision heredan en forma directa, y la clase EmpleadoBaseMasComision4 hereda en forma indirecta. Como pronto verá, al invocar el método ingresos de cada empleado desde una referencia a la superclase Empleado, se realiza el cálculo correcto de los ingresos gracias a las capacidades polimórficas de Java. Algunas veces, cuando se lleva a cabo el procesamiento polimórfico, es necesario programar “en forma específica”. Nuestro ejemplo práctico con Empleado demuestra que un programa puede determinar el tipo de un objeto en tiempo de ejecución, y actuar sobre ese objeto de manera acorde. En el ejemplo práctico utilizamos estas capacidades para determinar si cierto objeto empleado específico es un EmpleadoBaseMasComision. Si es así, incrementamos el salario base de ese empleado en un 10%. El capítulo continúa con una introducción a las interfaces en Java. Una interfaz describe a un conjunto de métodos que pueden llamarse en un objeto, pero no proporciona implementaciones concretas para ellos. Los programadores pueden declarar clases que implementen a (es decir, que proporcionen implementaciones concretas para los métodos de) una o más interfaces. Cada método de una interfaz debe declararse en todas las clases que implementen a la interfaz. Una vez que una clase implementa a una interfaz, todos los objetos de esa clase tienen una relación “es un” con el tipo de la interfaz, y se garantiza que todos los objetos de la clase proporcionarán la funcionalidad descrita por la interfaz. Esto se aplica también para todas las subclases de esa clase. En especial, las interfaces son útiles para asignar la funcionalidad común a clases que posiblemente no estén relacionadas. Esto permite que los objetos de clases no relacionadas se procesen en forma polimórfica; los objetos de las clases que implementan la misma interfaz pueden responder a las mismas llamadas a los métodos. Para demostrar la creación y el uso de interfaces, modificaremos nuestra aplicación de nómina para crear una aplicación general de cuentas por pagar, que puede calcular los pagos vencidos por los ingresos de los empleados de la compañía y los montos de las facturas a pagar por los bienes comprados. Como verá, las interfaces permiten capacidades polimórficas similares a las que permite la herencia.

10.2 Ejemplos del polimorfismo Ahora consideraremos diversos ejemplos adicionales. Si la clase Rectangulo se deriva de la clase Cuadrilatero, entonces un objeto Rectangulo es una versión más específica de un objeto Cuadrilatero. Cualquier operación (por ejemplo, calcular el perímetro o el área) que pueda realizarse en un objeto Cuadrilatero también puede realizarse en un objeto Rectangulo. Estas operaciones también pueden realizarse en otros objetos Cuadrilatero, como Cuadrado, Paralelogramo y Trapezoide. El polimorfismo ocurre cuando un programa invoca a un método a través de una variable de la superclase; en tiempo de ejecución, se hace una llamada a la versión correcta

www.elsolucionario.net

420

Capítulo 10

Programación orientada a objetos: polimorfismo

del método de la subclase, con base en el tipo de la referencia almacenada en la variable de la superclase. En la sección 10.3 veremos un ejemplo de código simple, en el cual se ilustra este proceso. Como otro ejemplo, suponga que diseñaremos un videojuego que manipule objetos de las clases Marciano, Venusino, Plutoniano, NaveEspacial y RayoLaser. Imagine que cada clase hereda de la superclase común llamada ObjetoEspacial, la cual contiene el método dibujar. Cada subclase implementa a este método. Un programa administrador de la pantalla mantiene una colección (por ejemplo, un arreglo ObjetoEspacial) de referencias a objetos de las diversas clases. Para refrescar la pantalla, el administrador de pantalla envía en forma periódica el mismo mensaje a cada objeto; a saber, dibujar. No obstante, cada objeto responde de una manera única. Por ejemplo, un objeto Marciano podría dibujarse a sí mismo en color rojo, con ojos verdes y el número apropiado de antenas. Un objeto NaveEspacial podría dibujarse a sí mismo como un platillo volador de color plata brillante; un objeto RayoLaser, como un rayo color rojo brillante a lo largo de la pantalla. De nuevo, el mismo mensaje (en este caso, dibujar) que se envía a una variedad de objetos tiene “muchas formas” de resultados. Un administrador de pantalla polimórfico podría utilizar el polimorfismo para facilitar el proceso de agregar nuevas clases a un sistema, con el menor número de modificaciones al código del sistema. Suponga que deseamos agregar objetos Mercuriano a nuestro videojuego. Para ello, debemos crear una clase Mercuriano que extienda a ObjetoEspacial y proporcione su propia implementación del método dibujar. Cuando aparezcan objetos de la clase Mercuriano en la colección ObjetoEspacial, el código del administrador de pantalla invocará al método dibujar, de la misma forma que para cualquier otro objeto en la colección, sin importar su tipo. Por lo tanto, los nuevos objetos Mercuriano simplemente se integran al videojuego sin necesidad de que el programador modifique el código del administrador de pantalla. Así, sin modificar el sistema (más que para crear nuevas clases y modificar el código que genera nuevos objetos), los programadores pueden utilizar el polimorfismo para incluir de manera conveniente tipos adicionales que no se hayan considerado a la hora de crear el sistema. Con el polimorfismo podemos usar el mismo nombre y la misma firma del método para hacer que ocurran distintas acciones, dependiendo del tipo del objeto en el que se invoca el método. Esto proporciona al programador una enorme capacidad expresiva.

Observación de ingeniería de software 10.1 El polimorfismo permite a los programadores tratar con las generalidades y dejar que el entorno en tiempo de ejecución se encargue de los detalles específicos. Los programadores pueden ordenar a los objetos que se comporten en formas apropiadas para ellos, sin necesidad de conocer los tipos de los objetos (siempre y cuando éstos pertenezcan a la misma jerarquía de herencia).

Observación de ingeniería de software 10.2 El polimorfismo promueve la extensibilidad: el software que invoca el comportamiento polimórfico es independiente de los tipos de los objetos a los cuales se envían los mensajes. Se pueden incorporar en un sistema nuevos tipos de objetos que pueden responder a las llamadas de los métodos existentes, sin necesidad de modificar el sistema base. Sólo el código cliente que crea instancias de los nuevos objetos debe modificarse para dar cabida a los nuevos tipos.

10.3 Demostración del comportamiento polimórfico En la sección 9.4 creamos una jerarquía de clases de empleados por comisión, en la cual la clase EmpleadoBaseMasComision heredó de la clase EmpleadoPorComision. Los ejemplos en esa sección manipularon objetos EmpleadoPorComision y EmpleadoBaseMasComision mediante el uso de referencias a ellos para invocar a sus métodos; dirigimos las referencias a la superclase a los objetos de la superclase, y las referencias a la subclase a los objetos de la subclase. Estas asignaciones son naturales y directas; las referencias a la superclase están diseñadas para referirse a objetos de la superclase, y las referencias a la subclase están diseñadas para referirse a objetos de la subclase. No obstante, como veremos pronto, es posible realizar otras asignaciones. En el siguiente ejemplo, dirigiremos una referencia a la superclase a un objeto de la subclase. Después mostraremos cómo al invocar un método en un objeto de la subclase a través de una referencia a la superclase se invoca a la funcionalidad de la subclase; el tipo del objeto actual al que se hace referencia, no el tipo de referencia, es el que determina cuál método se llamará. Este ejemplo demuestra el concepto clave de que un objeto de una subclase puede tratarse como un objeto de su superclase. Esto permite varias manipulaciones interesantes. Un programa puede crear un arreglo de referencias a la superclase, que se refieran a objetos de muchos tipos de sub-

www.elsolucionario.net

10.3

Demostración del comportamiento polimórfico

421

clases. Esto se permite, ya que cada objeto de una subclase es un objeto de su superclase. Por ejemplo, podemos asignar la referencia de un objeto EmpleadoBaseMasComision a una variable de la superclase EmpleadoPorComision, ya que un EmpleadoBaseMasComision es un EmpleadoPorComision; por lo tanto, podemos tratar a un EmpleadoBaseMasComision como un EmpleadoPorComision. Como veremos más adelante en este capítulo, no podemos tratar a un objeto de la superclase como un objeto de cualquiera de sus subclases, porque un objeto superclase no es un objeto de ninguna de sus subclases. Por ejemplo, no podemos asignar la referencia de un objeto EmpleadoPorComision a una variable de la subclase EmpleadoBaseMasComision, ya que un EmpleadoPorComision no es un EmpleadoBaseMasComision, no tiene una variable de instancia salarioBase y no tiene los métodos establecerSalarioBase y obtenerSalarioBase. La relación “es un” se aplica sólo de una subclase a sus superclases directas (e indirectas), pero no viceversa. El compilador de Java permite asignar una referencia a la superclase a una variable de la subclase, si convertimos explícitamente la referencia a la superclase al tipo de la subclase; una técnica que veremos con más detalle en la sección 10.5. ¿Para qué nos serviría, en un momento dado, realizar una asignación así? Una referencia a la superclase puede usarse para invocar sólo a los métodos declarados en la superclase; si tratamos de invocar métodos que sólo pertenezcan a la subclase, a través de una referencia a la superclase, se producen errores de compilación. Si un programa necesita realizar una operación específica para la subclase en un objeto de la subclase al que se haga una referencia mediante una variable de la superclase, el programa primero debe convertir la referencia a la superclase en una referencia a la subclase, mediante una técnica conocida como conversión descendente. Esto permite al programa invocar métodos de la subclase que no se encuentren en la superclase. En la sección 10.5 presentaremos un ejemplo concreto de conversión descendente. El ejemplo de la figura 10.1 demuestra tres formas de usar variables de la superclase y la subclase para almacenar referencias a objetos de la superclase y de la subclase. Las primeras dos formas son simples: al igual que en la sección 9.4, asignamos una referencia a la superclase a una variable de la superclase, y asignamos una referencia a la subclase a una variable de la subclase. Después demostramos la relación entre las subclases y las superclases (es decir, la relación “es-un” ) mediante la asignación de una referencia a la subclase a una variable de la superclase. [Nota: este programa utiliza las clases EmpleadoPorComision3 y EmpleadoBaseMasComision4 de las figuras 9.12 y 9.13, respectivamente].

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

// Fig. 10.1: PruebaPolimorfismo.java // Asignación de referencias a la superclase y la subclase, a // variables de la superclase y la subclase. public class PruebaPolimorfismo { public static void main( String args[] ) { // asigna la referencia a la superclase a una variable de la superclase EmpleadoPorComision3 empleadoPorComision = new EmpleadoPorComision3( "Sue", "Jones", "222-22-2222", 10000, .06 );

19 20 21 22 23

// asigna la referencia a la subclase a una variable de la subclase EmpleadoBaseMasComision4 empleadoBaseMasComision = new EmpleadoBaseMasComision4( "Bob", "Lewis", "333-33-3333", 5000, .04, 300 ); // invoca a toString en un objeto de la superclase, usando una variable de la superclase System.out.printf( "%s %s:\n\n%s\n\n", "Llamada a toString de EmpleadoPorComision3 con referencia de superclase ", "a un objeto de la superclase", empleadoPorComision.toString() ); // invoca a toString en un objeto de la subclase, usando una variable de la subclase

Figura 10.1 | Asignación de referencias a la superclase y la subclase, a variables de la superclase y la subclase. (Parte 1 de 2).

www.elsolucionario.net

422

24 25 26 27 28 29 30 31 32 33 34 35 36

Capítulo 10

Programación orientada a objetos: polimorfismo

System.out.printf( "%s %s:\n\n%s\n\n", "Llamada a toString de EmpleadoBaseMasComision4 con referencia", "de subclase a un objeto de la subclase" empleadoBaseMasComision.toString() ); // invoca a toString en un objeto de la subclase, usando una variable de la superclase EmpleadoPorComision3 empleadoPorComision2 = empleadoBaseMasComision; System.out.printf( “%s %s:\n\n%s\n”, "Llamada a toString de EmpleadoBaseMasComision4 con referencia de superclase", "a un objeto de la subclase", empleadoPorComision2.toString() ); } // fin de main } // fin de la clase PruebaPolimorfismo

Llamada a toString de EmpleadoPorComision3 con referencia de superclase a un objeto de la superclase: empleado por comision: Sue Jones numero de seguro social: 222-22-2222 ventas brutas: 10000.00 tarifa de comision: 0.06 Llamada a toString de EmpleadoBaseMasComision4 con referencia de subclase a un objeto de la subclase: con sueldo base empleado por comision: Bob Lewis numero de seguro social: 333-33-3333 ventas brutas: 5000.00 tarifa de comision: 0.04 sueldo base: 300.00 Llamada a toString de EmpleadoBaseMasComision4 con referencia de superclase a un objeto de la subclase: con sueldo base empleado por comision: Bob Lewis numero de seguro social: 333-33-3333 ventas brutas: 5000.00 tarifa de comision: 0.04 sueldo base: 300.00

Figura 10.1 | Asignación de referencias a la superclase y la subclase, a variables de la superclase y la subclase. (Parte 2 de 2). En la figura 10.1, las líneas 10 y 11 crean un objeto EmpleadoPorComision3 y asignan su referencia a una variable EmpleadoPorComision3. Las líneas 14 a 16 crean un objeto EmpleadoBaseMasComision4 y asignan su referencia a una variable EmpleadoBaseMasComision4. Estas asignaciones son naturales; por ejemplo, el principal propósito de una variable EmpleadoPorComision3 es guardar una referencia a un objeto EmpleadoPorComision3. Las líneas 19 a 21 utilizan la referencia empleadoPorComision para invocar a toString en forma explícita. Como empleadoPorComision hace referencia a un objeto EmpleadoPorComision3, se hace una llamada a la versión de toString de la superclase EmpleadoPorComision3. De manera similar, las líneas 24 a 27 utilizan a empleadoBaseMasComision para invocar a toString de forma explícita en el objeto EmpleadoBaseMasComision4. Esto invoca a la versión de toString de la subclase EmpleadoBaseMasComision4. Después, las líneas 30 y 31 asignan la referencia al objeto empleadoBaseMasComision de la subclase a una variable de la superclase EmpleadoPorComision3, que las líneas 32 a 34 utilizan para invocar al método toString. Cuando una variable de la superclase contiene una referencia a un objeto de la subclase, y esta referencia se utiliza para llamar a un método, se hace una llamada a la versión del método de la subclase. Por ende, empleadoPorComision2.ToString() en la línea 34 en realidad llama al método toString de la clase

www.elsolucionario.net

10.4

Clases y métodos abstractos

423

EmpleadoBaseMasComision4. El compilador de Java permite este “cruzamiento”, ya que un objeto de una subclase es un objeto de su superclase (pero no viceversa). Cuando el compilador encuentra una llamada a un método que se realiza a través de una variable, determina si el método puede llamarse verificando el tipo de clase de la variable. Si esa clase contiene la declaración del método apropiada (o hereda una), se compila la llamada. En tiempo de ejecución, el tipo del objeto al cual se refiere la variable es el que determina el método que se utilizará.

10.4 Clases y métodos abstractos Cuando pensamos en un tipo de clase, asumimos que los programas crearán objetos de ese tipo. No obstante, en algunos casos es conveniente declarar clases para las cuales el programador nunca creará instancias de objetos. A dichas clases se les conoce como clases abstractas. Como se utilizan sólo como superclases en jerarquías de herencia, nos referimos a ellas como superclases abstractas. Estas clases no pueden utilizarse para instanciar objetos, ya que como veremos pronto, las clases abstractas están incompletas. Las subclases deben declarar las “piezas faltantes”. En la sección 10.5 demostraremos las clases abstractas. El propósito de una clase abstracta es proporcionar una superclase apropiada, a partir de la cual puedan heredar otras clases y, por ende, compartir un diseño común. Por ejemplo, en la jerarquía de Figura de la figura 9.3, las subclases heredan la noción de lo que significa ser una Figura; los atributos comunes como posicion, color y grosorBorde, y los comportamientos como dibujar, mover, cambiarTamanio y cambiarColor. Las clases que pueden utilizarse para instanciar objetos se llaman clases concretas. Dichas clases proporcionan implementaciones de cada método que declaran (algunas de las implementaciones pueden heredarse). Por ejemplo, podríamos derivar las clases concretas Circulo, Cuadrado y Triangulo de la superclase abstracta FiguraBidimensional. De manera similar, podríamos derivar las clases concretas Esfera, Cubo y Tetraedro de la superclase abstracta FiguraTridimensional. Las superclases abstractas son demasiado generales como para crear objetos reales; sólo especifican lo que tienen en común las subclases. Necesitamos ser más específicos para poder crear objetos. Por ejemplo, si envía el mensaje dibujar a la clase abstracta FiguraBidimensional, la clase sabe que las figuras bidimensionales deben poder dibujarse, pero no sabe qué figura específica dibujar, por lo que no puede implementar un verdadero método dibujar. Las clases concretas proporcionan los detalles específicos que hacen razonable la creación de instancias de objetos. No todas las jerarquías de herencia contienen clases abstractas. Sin embargo, a menudo los programadores escriben código cliente que utiliza sólo tipos de superclases abstractas para reducir las dependencias del código cliente en un rango de tipos de subclases específicas. Por ejemplo, un programador puede escribir un método con un parámetro de un tipo de superclase abstracta. Cuando se llama, ese método puede recibir un objeto de cualquier clase concreta que extienda en forma directa o indirecta a la superclase especificada como el tipo del parámetro. Algunas veces las clases abstractas constituyen varios niveles de la jerarquía. Por ejemplo, la jerarquía de Figura de la figura 9.3 empieza con la clase abstracta Figura. En el siguiente nivel de la jerarquía hay dos clases abstractas más, FiguraBidimensional y FiguraTridimensional. El siguiente nivel de la jerarquía declara clases concretas para objetos FiguraBidimensional (Circulo, Cuadrado y Triangulo) y para objetos FiguraTridimensional (Esfera, Cubo y Tetraedro). Para hacer una clase abstracta, ésta se declara con la palabra clave abstract. Por lo general, una clase abstracta contiene uno o más métodos abstractos. Un método abstracto tiene la palabra clave abstract en su declaración, como en public abstract void dibujar(); // método abstracto

Los métodos abstractos no proporcionan implementaciones. Una clase que contiene métodos abstractos debe declararse como clase abstracta, aun si esa clase contiene métodos concretos (no abstractos). Cada subclase concreta de una superclase abstracta también debe proporcionar implementaciones concretas de los métodos abstractos de la superclase. Los constructores y los métodos static no pueden declararse como abstract. Los constructores no se heredan, por lo que nunca podría implementarse un constructor abstract. Aunque los métodos static se heredan, no están asociados con objetos específicos de las clases que los declaran. Como el propósito de los métodos abstract es sobrescribirlos para procesar objetos con base en sus tipos, no tendría sentido declarar un método static como abstract.

www.elsolucionario.net

424

Capítulo 10

Programación orientada a objetos: polimorfismo

Observación de ingeniería de software 10.3 Una clase abstracta declara los atributos y comportamientos comunes de las diversas clases en una jerarquía de clases. Por lo general, una clase abstracta contiene uno o más métodos abstractos, que las subclases deben sobrescribir, si van a ser concretas. Las variables de instancia y los métodos concretos de una clase abstracta están sujetos a las reglas normales de la herencia,

Error común de programación 10.1 Tratar de instanciar un objeto de una clase abstracta es un error de compilación.

Error común de programación 10.2 Si no se implementan los métodos abstractos de la superclase en una clase derivada, se produce un error de compilación, a menos que la clase derivada también se declare como abstract.

Aunque no podemos instanciar objetos de superclases abstractas, pronto veremos que podemos usar superclases abstractas para declarar variables que puedan guardar referencias a objetos de cualquier clase concreta que se derive de esas superclases abstractas. Por lo general, los programas utilizan dichas variables para manipular los objetos de las subclases mediante el polimorfismo. Además, podemos usar los nombres de las superclases abstractas para invocar métodos static que estén declarados en esas superclases abstractas. Considere otra aplicación del polimorfismo. Un programa de dibujo necesita mostrar en pantalla muchas figuras, incluyendo nuevos tipos de figuras que el programador agregará al sistema después de escribir el programa de dibujo. Este programa podría necesitar mostrar figuras, como Circulos, Triangulos, Rectangulos u otras, que se deriven de la superclase abstracta Figura. El programa de dibujo utiliza variables de Figura para administrar los objetos que se muestran en pantalla. Para dibujar cualquier objeto en esta jerarquía de herencia, el programa de dibujo utiliza una variable de la superclase Figura que contiene una referencia al objeto de la subclase para invocar al método dibujar del objeto. Este método se declara como abstract en la superclase Figura, por lo que cada subclase concreta debe implementar el método dibujar en una forma que sea específica para esa figura. Cada objeto en la jerarquía de herencia de Figura sabe cómo dibujarse a sí mismo. El programa de dibujo no tiene que preocuparse acerca del tipo de cada objeto, o si ha encontrado objetos de ese tipo. En especial, el polimorfismo es efectivo para implementar los denominados sistemas de software en capas. Por ejemplo, en los sistemas operativos cada tipo de dispositivo físico puede operar en forma muy distinta a los demás. Aun así, los comandos para leer o escribir datos desde y hacia los dispositivos pueden tener cierta uniformidad. Para cada dispositivo, el sistema operativo utiliza una pieza de software llamada controlador de dispositivos para controlar toda la comunicación entre el sistema y el dispositivo. El mensaje de escritura que se envía a un objeto controlador de dispositivo necesita interpretarse de manera específica en el contexto de ese controlador, y la forma en que manipula a un dispositivo de un tipo específico. No obstante, la llamada de escritura en sí no es distinta a la escritura en cualquier otro dispositivo en el sistema: colocar cierto número de bytes de memoria en ese dispositivo. Un sistema operativo orientado a objetos podría usar una superclase abstracta para proporcionar una “interfaz” apropiada para todos los controladores de dispositivos. Después, a través de la herencia de esa superclase abstracta, se forman clases derivadas que se comporten todas de manera similar. Los métodos del controlador de dispositivos se declaran como métodos abstractos en la superclase abstract. Las implementaciones de estos métodos abstractos se proporcionan en las subclases que corresponden a los tipos específicos de controladores de dispositivos. Siempre se están desarrollando nuevos dispositivos, a menudo mucho después de que se ha liberado el sistema operativo. Cuando usted compra un nuevo dispositivo, éste incluye un controlador de dispositivo proporcionado por el distribuidor. El dispositivo opera de inmediato, una vez que usted lo conecta a la computadora e instala el controlador de dispositivo. Éste es otro elegante ejemplo acerca de cómo el polimorfismo hace que los sistemas sean extensibles. En la programación orientada a objetos es común declarar una clase iteradora que pueda recorrer todos los objetos en una colección, como un arreglo (capítulo 7) o un objeto ArrayList (capítulo 19, Colecciones). Por ejemplo, un programa puede imprimir un arreglo ArrayList de objetos creando un objeto iterador, y luego usándolo para obtener el siguiente elemento de la lista cada vez que se llame al iterador. Los iteradores se utilizan comúnmente en la programación polimórfica para recorrer una colección que contiene referencias a objetos de diversos niveles de una jerarquía. (El capítulo 19 presenta un tratamiento detallado de ArrayList, los iteradores y las capacidades “genéricas”). Por ejemplo, un arreglo ArrayList de objetos de la clase FiguraBidimensional

www.elsolucionario.net

10.5

Ejemplo práctico: sistema de nómina utilizando polimorfismo

425

podría contener objetos de las subclases Cuadrado, Circulo, Triangulo y así, sucesivamente. Al llamar al método dibujar para cada objeto FiguraBidimensional mediante una variable FiguraBidimensional, se dibujaría en forma polimórfica a cada objeto correctamente en la pantalla.

10.5 Ejemplo práctico: sistema de nómina utilizando polimorfismo En esta sección analizamos de nuevo la jerarquía EmpleadoPorComision-EmpleadoBaseMasComision que exploramos a lo largo de la sección 9.4. Ahora podemos usar un método abstracto y polimorfismo para realizar cálculos de nómina, con base en el tipo de empleado. Crearemos una jerarquía de empleados mejorada para resolver el siguiente problema: Una compañía paga a sus empleados por semana. Los empleados son de cuatro tipos: empleados asalariados que reciben un salario semanal fijo, sin importar el número de horas trabajadas; empleados por horas, que reciben un sueldo por hora y pago por tiempo extra, por todas las horas trabajadas que excedan a 40 horas; empleados por comisión, que reciben un porcentaje de sus ventas y empleados asalariados por comisión, que reciben un salario base más un porcentaje de sus ventas. Para este periodo de pago, la compañía ha decidido recompensar a los empleados asalariados por comisión, agregando un 10% a sus salarios base. La compañía desea implementar una aplicación en Java que realice sus cálculos de nómina en forma polimórfica.

Utilizaremos la clase abstract Empleado para representar el concepto general de un empleado. Las clases que extienden a Empleado son EmpleadoAsalariado, EmpleadoPorComision y EmpleadoPorHoras. La clase EmpleadoBaseMasComision (que extiende a EmpleadoPorComision) representa el último tipo de empleado. El diagrama de clases de UML en la figura 10.2 muestra la jerarquía de herencia para nuestra aplicación polimórfica de nómina de empleados. Observe que la clase abstracta Empleado está en cursivas, según la convención de UML. La superclase abstracta Empleado declara la “interfaz” para la jerarquía; esto es, el conjunto de métodos que puede invocar un programa en todos los objetos Empleado. Aquí utilizamos el término “interfaz” en un sentido general, para referirnos a las diversas formas en que los programas pueden comunicarse con los objetos de cualquier subclase de Empleado. Tenga cuidado de no confundir la noción general de una “interfaz” con la noción formal de una interfaz en Java, el tema de la sección 10.7. Cada empleado, sin importar la manera en que se calculen sus ingresos, tiene un primer nombre, un apellido paterno y un número de seguro social, por lo que las variables de instancia private primerNombre, apellidoPaterno y numeroSeguroSocial aparecen en la superclase abstracta Empleado.

Observación de ingeniería de software 10.4 Una subclase puede heredar la “interfaz” o “implementación” de una superclase. Las jerarquías diseñadas para la herencia de implementación tienden a tener su funcionalidad en niveles altos de la jerarquía; cada nueva subclase hereda uno o más métodos que se implementaron en una superclase, y la subclase utiliza las implementaciones de la superclase. Las jerarquías diseñadas para la herencia de interfaz tienden a tener su funcionalidad en niveles bajos de la jerarquía; una superclase especifica uno o más métodos abstractos que deben declararse para cada clase concreta en la jerarquía, y las subclases individuales sobrescriben estos métodos para proporcionar la implementación específica para cada subclase.

Empleado

EmpleadoAsalariado

EmpleadoPorComisión

EmpleadoPorHoras

EmpleadoBaseMasComision

Figura 10.2 | Diagrama de clases de UML para la jerarquía de Empleado.

www.elsolucionario.net

426

Capítulo 10

Programación orientada a objetos: polimorfismo

Las siguientes secciones implementan la jerarquía de clases de Empleado. Cada una de las primeras cuatro secciones implementa una de las clases concretas. La última sección implementa un programa de prueba que crea objetos de todas estas clases y procesa esos objetos mediante el polimorfismo.

10.5.1 Creación de la superclase abstracta Empleado La clase Empleado (figura 10.4) proporciona los métodos ingresos y toString, además de los métodos obtener y establecer que manipulan las variables de instancia de Empleado. Es evidente que un método ingresos se aplica en forma genérica a todos los empleados. Pero cada cálculo de los ingresos depende de la clase de empleado. Por lo tanto, declaramos a ingresos como abstract en la superclase Empleado, ya que una implementación predeterminada no tiene sentido para ese método; no hay suficiente información para determinar qué monto debe devolver ingresos. Cada una de las subclases redefine a ingresos con una implementación apropiada. Para calcular los ingresos de un empleado, la aplicación asigna una referencia al objeto de empleado a una variable de la superclase Empleado, y después invoca al método ingresos en esa variable. Mantenemos un arreglo de variables Empleado, cada una de las cuales guarda una referencia a un objeto Empleado (desde luego que no puede haber objetos Empleado, ya que ésta es una clase abstracta; sin embargo, debido a la herencia todos los objetos de todas las subclases de Empleado pueden considerarse como objetos Empleado). El programa itera a través del arreglo y llama al método ingresos para cada objeto Empleado. Java procesa estas llamadas a los métodos en forma polimórfica. Al incluir a ingresos como un método abstracto en Empleado, se obliga a cada subclase directa de Empleado a sobrescribir el método ingresos para poder convertirse en una clase concreta. Esto permite al diseñador de la jerarquía de clases demandar que cada subclase concreta proporcione un cálculo apropiado del sueldo. El método toString en la clase Empleado devuelve un objeto String que contiene el primer nombre, el apellido paterno y el número de seguro social del empleado. Como veremos, cada subclase de Empleado sobrescribe el método toString para crear una representación String de un objeto de esa clase que contiene el tipo del empleado (por ejemplo, "empleado asalariado:"), seguido del resto de la información del empleado. El diagrama en la figura 10.3 muestra cada una de las cinco clases en la jerarquía, hacia abajo en la columna de la izquierda, y los métodos ingresos y toString en la fila superior. Para cada clase, el diagrama muestra los resultados deseados de cada método. [Nota: no listamos los métodos establecer y obtener de la superclase Empleado porque no se redefinen en ninguna de las subclases; cada una de estas propiedades se hereda y cada una de las subclases las utiliza “como están”]. Consideremos ahora la declaración de la clase Empleado (figura 10.4). Esta clase incluye un constructor que recibe el primer nombre, el apellido paterno y el número de seguro social como argumentos (líneas 11 a 16); los métodos obtener que devuelven el primer nombre, apellido y número de seguro social (líneas 25 a 28, 37 a 40 y 49 a 52, respectivamente); los métodos establecer que establecen el primer nombre, el apellido paterno y el número de seguro social (líneas 19 a 22, 31 a 34 y 43 a 46, respectivamente); el método toString (líneas 55 a 59), el cual devuelve la representación String de Empleado; y el método abstract ingresos (línea 62), que las subclases deben implementar. Observe que el constructor de Empleado no valida el número de seguro social en este ejemplo. Por lo general, se debe proporcionar esa validación. ¿Por qué declaramos a ingresos como un método abstracto? Simplemente, no tiene sentido proporcionar una implementación de este método en la clase Empleado. No podemos calcular los ingresos para un Empleado general; primero debemos conocer el tipo de Empleado específico para determinar el cálculo apropiado de los ingresos. Al declarar este método abstract, indicamos que cada subclase concreta debe proporcionar una implementación apropiada para ingresos, y que un programa podrá utilizar las variables de la superclase Empleado para invocar al método ingresos en forma polimórfica, para cualquier tipo de Empleado.

10.5.2 Creación de la subclase concreta EmpleadoAsalariado La clase EmpleadoAsalariado (figura 10.5) extiende a la clase Empleado (línea 4) y redefine a ingresos (líneas 29 a 32), lo cual convierte a EmpleadoAsalariado en una clase concreta. La clase incluye un constructor (líneas 9 a 14) que recibe un primer nombre, un apellido paterno, un número de seguro social y un salario semanal como argumentos; un método establecer para asignar un valor positivo a la variable de instancia salarioSemanal (líneas 17 a 20); un método obtener para devolver el valor de salarioSemanal (líneas 23 a 26); un método ingresos (líneas 29 a 32) para calcular los ingresos de un EmpleadoAsalariado; y un método toString (líneas 35 a 39) que devuelve un objeto String que incluye el tipo del empleado, a saber, "empleado asalariado: ", seguido

www.elsolucionario.net

10.5

Ejemplo práctico: sistema de nómina utilizando polimorfismo

ingresos

toString

Empleado

abstract

primerNombre apellidoPaterno número de seguro social: NSS

EmpleadoAsalariado

salarioSemanal

empleado asalariado: primerNombre apellidoPaterno número de seguro social: NSS salario semanal: salarioSemanal

EmpleadoPorHoras

if horas 40 40 * sueldo + ( horas - 40 ) * sueldo * 1.5

empleado por horas: primerNombre apellidoPaterno número de seguro social: NSS sueldo por horas: sueldo; horas trabajadas: horas

EmpleadoPorComisión

tarifaComisión * ventasBrutas

empleado por comisión: primerNombre apellidoPaterno número de seguro social: NSS ventas brutas: ventasBrutas; tarifa de comisión: tarifaComisión

( tarifaComision * ventasBrutas ) + salarioBase

empleado por comisión con salario base: primerNombre apellidoPaterno número de seguro social: NSS ventas brutas: ventasBrutas; tarifa de comisión: tarifaComision; salario base: salarioBase

EmpleadoBaseMasComision

Figura 10.3 | Interfaz polimórfica para las clases de la jerarquía de Empleado.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

// Fig. 10.4: Empleado.java // La superclase abstracta Empleado. public abstract class Empleado { private String primerNombre; private String apellidoPaterno; private String numeroSeguroSocial; // constructor con tres argumentos public Empleado( String nombre, String apellido, String nss ) { primerNombre = nombre; apellidoPaterno = apellido; numeroSeguroSocial = nss; } // fin del constructor de Empleado con tres argumentos // establece el primer nombre public void establecerPrimerNombre( String nombre ) { primerNombre = nombre; } // fin del método establecerPrimerNombre // devuelve el primer nombre public String obtenerPrimerNombre()

Figura 10.4 | La superclase abstracta Empleado. (Parte 1 de 2).

www.elsolucionario.net

427

428

26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

Capítulo 10

Programación orientada a objetos: polimorfismo

{ return primerNombre; } // fin del método obtenerPrimerNombre // establece el apellido paterno public void establecerApellidoPaterno( String apellido ) { apellidoPaterno = apellido; } // fin del método establecerApellidoPaterno // devuelve el apellido paterno public String obtenerApellidoPaterno() { return apellidoPaterno; } // fin del método obtenerApellidoPaterno // establece el número de seguro social public void establecerNumeroSeguroSocial( String nss ) { numeroSeguroSocial = nss; // debe validar } // fin del método establecerNumeroSeguroSocial // devuelve el número de seguro social public String obtenerNumeroSeguroSocial() { return numeroSeguroSocial; } // fin del método obtenerNumeroSeguroSocial // devuelve representación String de un objeto Empleado public String toString() { return String.format( "%s %s\nnumero de seguro social: %s", obtenerPrimerNombre(), obtenerApellidoPaterno(), obtenerNumeroSeguroSocial() ); } // fin del método toString // método abstracto sobrescrito por las subclases public abstract double ingresos(); // aquí no hay implementación } // fin de la clase abstracta Empleado

Figura 10.4 | La superclase abstracta Empleado. (Parte 2 de 2).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

// Fig. 10.5: EmpleadoAsalariado.java // La clase EmpleadoAsalariado extiende a Empleado. public class EmpleadoAsalariado extends Empleado { private double salarioSemanal; // constructor de cuatro argumentos public EmpleadoAsalariado( String nombre, String apellido, String nss, double salario ) { super( nombre, apellido, nss ); // los pasa al constructor de Empleado establecerSalarioSemanal( salario ); // valida y almacena el salario } // fin del constructor de EmpleadoAsalariado con cuatro argumentos // establece el salario

Figura 10.5 | La clase EmpleadoAsalariado derivada de Empleado. (Parte 1 de 2).

www.elsolucionario.net

10.5

17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

Ejemplo práctico: sistema de nómina utilizando polimorfismo

429

public void establecerSalarioSemanal( double salario ) { salarioSemanal = salario < 0.0 ? 0.0 : salario; } // fin del método establecerSalarioSemanal // devuelve el salario public double obtenerSalarioSemanal() { return salarioSemanal; } // fin del método obtenerSalarioSemanal // calcula los ingresos; sobrescribe el método abstracto ingresos en Empleado public double ingresos() { return obtenerSalarioSemanal(); } // fin del método ingresos // devuelve representación String de un objeto EmpleadoAsalariado public String toString() { return String.format( "empleado asalariado: %s\n%s: $%,.2f", super.toString(), "salario semanal", obtenerSalarioSemanal() ); } // fin del método toString } // fin de la clase EmpleadoAsalariado

Figura 10.5 | La clase EmpleadoAsalariado derivada de Empleado. (Parte 2 de 2).

de la información específica para el empleado producida por el método toString de la superclase Empleado y el método obtenerSalarioSemanal de EmpleadoAsalariado. El constructor de la clase EmpleadoAsalariado pasa el primer nombre, el apellido paterno y el número de seguro social al constructor de Empleado (línea 12) para inicializar las variables de instancia private que no se heredan de la superclase. El método ingresos sobrescribe el método abstracto ingresos de Empleado para proporcionar una implementación concreta que devuelva el salario semanal del EmpleadoAsalariado. Si no implementamos ingresos, la clase EmpleadoAsalariado debe declararse como abstract; en caso contrario, se produce un error de compilación (y desde luego, queremos que EmpleadoAsalariado sea una clase concreta). El método toString (líneas 35 a 39) de la clase EmpleadoAsalariado sobrescribe al método toString de Empleado. Si la clase EmpleadoAsalariado no sobrescribiera a toString, EmpleadoAsalariado habría heredado la versión de toString de Empleado. En ese caso, el método toString de EmpleadoAsalariado simplemente devolvería el nombre completo del empleado y su número de seguro social, lo cual no representa en forma adecuada a un EmpleadoAsalariado. Para producir una representación String completa de EmpleadoAsalariado, el método toString de la subclase devuelve "empleado asalariado: ", seguido de la información específica de la clase base Empleado (es decir, el primer nombre, el apellido paterno y el número de seguro social) que se obtiene al invocar el método toString de la superclase (línea 38); éste es un excelente ejemplo de reutilización de código. La representación String de un EmpleadoAsalariado también contiene el salario semanal del empleado, el cual se obtiene mediante la invocación del método obtenerSalarioSemanal de la clase.

10.5.3 Creación de la subclase concreta EmpleadoPorHoras La clase EmpleadoPorHoras (figura 10.6) también extiende a Empleado (línea 4). La clase incluye un constructor (líneas 10 a 16) que recibe como argumentos un primer nombre, un apellido paterno, un número de seguro social, un sueldo por horas y el número de horas trabajadas. Las líneas 19 a 22 y 31 a 35 declaran los métodos establecer que asignan nuevos valores a las variables de instancia sueldo y horas, respectivamente. El método establecerSueldo (líneas 19 a 22) asegura que sueldo sea positivo, y el método establecerHoras (líneas 31 a 35) asegura que horas esté entre 0 y 168 (el número total de horas en una semana), inclusive. La clase EmpleadoPorHoras también incluye métodos obtener (líneas 25 a 28 y 38 a 41) para devolver los valores de sueldo y horas, respectivamente; un método ingresos (líneas 44 a 50) para calcular los ingresos de un EmpleadoPorHo-

www.elsolucionario.net

430

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

Capítulo 10

Programación orientada a objetos: polimorfismo

// Fig. 10.6: EmpleadoPorHoras.java // La clase EmpleadoPorHoras extiende a Empleado. public class EmpleadoPorHoras extends Empleado { private double sueldo; // sueldo por hora private double horas; // horas trabajadas por semana // constructor con cinco argumentos public EmpleadoPorHoras( String nombre, String apellido, String nss, double sueldoPorHoras, double horasTrabajadas ) { super( nombre, apellido, nss ); establecerSueldo( sueldoPorHoras ); // valida y almacena el sueldo por horas establecerHoras( horasTrabajadas ); // valida y almacena las horas trabajadas } // fin del constructor de EmpleadoPorHoras con cinco argumentos // establece el sueldo public void establecerSueldo( double sueldoPorHoras ) { sueldo = ( sueldoPorHoras < 0.0 ) ? 0.0 : sueldoPorHoras; } // fin del método establecerSueldo // devuelve el sueldo public double obtenerSueldo() { return sueldo; } // fin del método obtenerSueldo // establece las horas trabajadas public void establecerHoras( double horasTrabajadas ) { horas = ( ( horasTrabajadas >= 0.0 ) && ( horasTrabajadas >> genera el evento externo que copia el texto seleccionado en el objeto JTextArea de la izquierda, y lo muestra en el objeto JTextArea de la derecha.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49

// Fig. 11.47: MarcoAreaTexto.java // Copia el texto seleccionado de un área de texto a otra. import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import javax.swing.Box; import javax.swing.JFrame; import javax.swing.JTextArea; import javax.swing.JButton; import javax.swing.JScrollPane; public class MarcoAreaTexto extends JFrame { private JTextArea areaTexto1; // muestra cadena de demostración private JTextArea areaTexto2; // el texto resaltado se copia aquí private JButton botonCopiar; // inicia el copiado de texto // constructor sin argumentos public MarcoAreaTexto() { super( "Demostracion de JTextArea" ); Box cuadro = Box.createHorizontalBox(); // crea un cuadro String demo = "Esta es una cadena de\ndemostracion para\n" + "ilustrar como copiar texto\nde un area de texto a \n" + "otra, usando un\nevento externo\n"; areaTexto1 = new JTextArea( demo, 10, 15 ); // crea área de texto 1 cuadro.add( new JScrollPane( areaTexto1 ) ); // agrega panel de desplazamiento botonCopiar = new JButton( "Copiar >>>" ); // crea botón para copiar cuadro.add( botonCopiar ); // agrega botón de copia al cuadro botonCopiar.addActionListener( new ActionListener() // clase interna anónima { // establece el texto en areaTexto2 con el texto seleccionado de areaTexto1 public void actionPerformed( ActionEvent evento ) { areaTexto2.setText( areaTexto1.getSelectedText() ); } // fin del método actionPerformed } // fin de la clase interna anónima ); // fin de la llamada a addActionListener areaTexto2 = new JTextArea( 10, 15 ); // crea segunda área de texto areaTexto2.setEditable( false ); // deshabilita edición cuadro.add( new JScrollPane( areaTexto2 ) ); // agrega panel de desplazamiento add( cuadro ); // agrega cuadro al marco } // fin del constructor de MarcoAreaTexto } // fin de la clase MarcoAreaTexto

Figura 11.47 | Copiado de texto seleccionado, de un objeto JTextArea a otro.

www.elsolucionario.net

11.19 JTextArea

1 2 3 4 5 6 7 8 9 10 11 12 13 14

525

// Fig. 11.48: DemoAreaTexto.java // Copia el texto seleccionado de un área de texto a otra. import javax.swing.JFrame; public class DemoAreaTexto { public static void main( String args[] ) { MarcoAreaTexto marcoAreaTexto = new MarcoAreaTexto(); marcoAreaTexto.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); marcoAreaTexto.setSize( 425, 200 ); // establece el tamaño del marco marcoAreaTexto.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase DemoAreaTexto

Figura 11.48 | Copiado de texto seleccionado, de un objeto TextAreaFrame. 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 22.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 JTextArea 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 manera 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 ActionEvent 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 botonCopiar, 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 11.17 vimos que el esquema predeterminado de un objeto JFrame es BorderLayout, y que el método add adjunta de manera predeterminada su argumento a la región CENTER de este esquema. Algunas veces es conveniente, cuando el texto llega al lado derecho de un objeto JTextArea, hacer que se recorra 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.

www.elsolucionario.net

526

Capítulo 11

Archivo Nuevo Abrir... Cerrar

Componentes de la GUI: parte 1

Observación de apariencia visual 11.20 Para proporcionar la funcionalidad de envoltura de líneas para un objeto neWrap de JTextArea con un argumento true.

JTextArea,

invoque el método setLi-

Políticas de las barras de desplazamiento de JScrollPane En 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 JScrollPane, puede usar los métodos setHorizontalScrollBarPolicy y setVerticalScrollBarPolicy de JScrollPane para modificar las políticas de las barras redesplazamiento en cualquier momento. La clase JScrollPane declara las constantes JScrollPane.VERTICAL_SCROLLBAR_ALWAYS JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS

para indicar que siempre debe aparecer una barra de desplazamiento, las constantes JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED JScrollPane.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_NEVER JScrollPane.HORIZONTAL_SCROLLBAR_NEVER

para indicar que nunca debe aparecer una barra de desplazamiento. Si la política de la barra de desplazamiento horizontal se establece en JScrollPane.HORIZONTAL_SCROLLBAR_NEVER, un objeto JTextArea adjunto al objeto JScrollPane envolverá las líneas de manera automática.

11.20 Conclusión En 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. Aprendió 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. También aprendió 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 GUIs 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 22, Componentes de la GUI: parte 2, aprenderá acerca de los componentes de GUI más avanzados, como los botones deslizables, los menús y los administradores de esquemas más complicados. 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.

Resumen Sección 11.1 Introducción • Una 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. • Al 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 aplicación, de manera que pueden aprender a utilizarla en menor tiempo y con mayor productividad.

www.elsolucionario.net

Resumen

527

• Las GUIs se crean a partir de componentes de GUI; a éstos se les conoce algunas veces como controles o “widgets”.

Sección 11.2 Entrada/salida simple basada en GUI con JOptionPane • La mayoría de las aplicaciones utilizan ventanas o cuadros de diálogo (también conocidos como diálogos) para interactuar con el usuario. • La clase JOptionPane de Java (paquete javax.swing) proporciona cuadros de diálogo preempaquetados para entrada y salida. El método static showInputDialog de JOptionPane muestra un diálogo de entrada. • 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 de JOptionPane muestra un diálogo de mensaje.

Sección 11.3 Generalidades de los componentes de Swing • La mayoría de los componentes de GUI de Swing se encuentran en el paquete javax.swing. Forman parte de las Java Foundation Classes (JFC): las bibliotecas de Java para el desarrollo de GUIs en distintas plataformas. • 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. Los componentes de GUI de Swing nos permiten especificar 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 soporte la plataforma subyacente en la que se ejecuta una aplicación. • Varios componentes de Swing son componentes pesados, que requieren una interacción directa con el sistema de ventanas local, lo cual puede restringir su apariencia y funcionalidad. • La clase Component (paquete java.awt) declara muchos de los atributos y comportamientos comunes para los componentes de GUI en los paquetes java.awt y javax.swing. • La clase Container (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 (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, teclas de método abreviado llamadas nemónicos, cuadros de información sobre herramientas, soporte para tecnologías de ayuda y soporte para la localización de la interfaz de usuario.

Sección 11.4 Mostrar texto e imágenes en una ventana • La mayoría de las ventanas son instancias de la clase JFrame o una subclase de JFrame. JFrame proporciona los atributos y comportamientos básicos de una ventana. • Un objeto JLabel 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. • Al crear una GUI, cada componente de ésta debe adjuntarse a un contenedor, como una ventana creada con un objeto JFrame. • Muchos IDEs proporcionan herramientas de diseño de GUIs, en las cuales podemos especificar el tamaño y la ubicación exactos de un componente mediante el uso del ratón, y después el IDE genera el código de la GUI por nosotros. • El método setToolTipText de JComponent especifica la información sobre herramientas que se muestra cuando el usuario coloca el cursor del ratón sobre un componente ligero. • El método add de Container adjunta un componente de GUI a un objeto Container. • La clase ImageIcon (paquete javax.swing) soporta varios formatos de imagen, incluyendo GIF, PNG y JPEG. • El método getClass (de la clase Object) 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 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. • La interfaz SwingConstants (paquete javax.swing) declara un conjunto de constantes enteras comunes que se utilizan con muchos componentes de Swing. • Las alineaciones horizontal y vertical de un objeto JLabel se pueden establecer mediante los métodos setHorizontalAlignment y setVerticalAlignment, respectivamente. • El método setText de JLabel establece el texto a mostrar en una etiqueta. El correspondiente método getText obtiene el texto actual que se muestra en una etiqueta.

www.elsolucionario.net

528

Capítulo 11

Componentes de la GUI: parte 1

• El método setIcon de JLabel especifica el objeto Icon a mostrar en una etiqueta. El correspondiente método getIcon obtiene el objeto Icon actual que se muestra en una etiqueta. • Los métodos setHorizontalTextPosition y setVerticalTextPosition de JLabel especifican la posición del texto en la etiqueta. • El método setDefaultCloseOperation de JFrame, con la constante JFrame.EXIT_ON_CLOSE como argumento, indica que el programa debe terminar cuando el usuario cierre la ventana. • El método setSize de Component especifica la anchura y la altura de un componente. • El método setVisible de Component con el argumento true muestra un objeto JFrame en la pantalla.

Sección 11.5 Campos de texto y una introducción al manejo de eventos con clases anidadas • Las GUIs se controlan por eventos; cuando el usuario interactúa con un componente de GUI, los eventos controlan al programa para realizar las tareas. • El código que realiza una tarea en respuesta a un evento se llama manejador de eventos, y el proceso general de responder a los eventos se conoce como manejo de eventos. • La clase JTextField extiende a la clase JTextComponent (paquete javax.swing.text), que proporciona muchas características comunes para los componentes de Swing basados en texto. La clase JPasswordField extiende a JTextField y agrega varios métodos específicos para el procesamiento de contraseñas. • Un objeto JPasswordField muestra que se están escribiendo caracteres a medida que el usuario los introduce, pero oculta los caracteres reales con caracteres de eco. • Un componente recibe el enfoque cuando el usuario hace clic sobre él. • El método setEditable de JTextComponent puede usarse para hacer que un campo de texto no pueda editarse. • Antes de que una aplicación pueda responder a un evento para un componente específico de la GUI, debemos realizar varios pasos de codificación: 1) Crear una clase que represente al manejador de eventos. 2) Implementar una interfaz apropiada, conocida como interfaz de escucha de eventos, en la clase del paso 1. 3) Indicar que se debe notificar a un objeto de la clase de los pasos 1 y 2 cuando ocurra el evento. A esto se le conoce como registrar el manejador de eventos. • Las clases anidadas pueden ser static o no static. Las clases anidadas no static se llaman clases internas, y se utilizan con frecuencia para el manejo de eventos. • Antes de poder crear un objeto de una clase interna, debe haber primero un objeto de la clase de nivel superior, que contenga a la clase interna, ya que un objeto de la clase interna tiene de manera implícita una referencia a un objeto de su clase de nivel superior. • Un objeto de la clase interna puede acceder directamente a todas las variables de instancia 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 o JPasswordField, el componente de la GUI genera un evento ActionEvent (paquete java.awt.event). Dicho evento se procesa mediante un objeto que implementa a la interfaz ActionListener (paquete java.awt.event). • El método addActionListener de JTextField registra el manejador de eventos para un campo de texto de un componente. Este método recibe como argumento un objeto ActionListener. • El componente de GUI con el que interactúa el usuario es el origen del evento. • 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 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 devuelve la contraseña que escribió el usuario.

Sección 11.6 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. 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.

Sección 11.7 Cómo funciona el manejo de eventos • 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 componente de escucha.

www.elsolucionario.net

Resumen

529

• Todo objeto JComponent tiene una variable de instancia llamada listenerList, la cual hace referencia a un objeto de la clase EventListenerList (paquete javax.swing.event). Cada objeto de una subclase de JComponent mantiene las referencias a todos sus componentes de escucha registrados en la variable listenerList. • Todo componente de la GUI soporta varios tipos de eventos, incluyendo los eventos de ratón, de teclado y otros. Cuando ocurre un evento, éste se despacha sólo a los componentes de escucha de eventos del tipo apropiado. El componente de la GUI recibe un ID de evento único, especificando el tipo de evento, el cual utiliza para decidir el tipo de componente de escucha al que debe despacharse el evento, y cuál método llamar en cada objeto componente de escucha.

Sección 11.8 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 (paquete javax.swing), la cual declara las características comunes para los botones de Swing. Por lo general, las etiquetas de los botones usan la capitalización tipo 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. • Los botones de comandos se crean con la clase JButton. • Un objeto JButton puede mostrar un objeto Icon. Para proporcionar al usuario un nivel adicional de interacción visual con la GUI, un objeto JButton también puede tener un icono de sustitución: un objeto Icon que se muestra cuando el usuario coloca el ratón sobre el botón. • El método setRolloverIcon (de la clase AbstractButton) especifica la imagen a mostrar en un botón, cuando el usuario coloca el ratón sobre él.

Sección 11.9 Botones que mantienen el estado • Los componentes de la GUI de Swing contienen tres tipos de botones de estado: JToggleButton, JCheckBox y JRadioButton. • Las clases JCheckBox y JRadioButton son subclases de JToggleButton. Un objeto JRadioButton es distinto de un objeto JCheckBox en cuanto a que, generalmente, hay varios objetos JRadioButton que se agrupan, y sólo puede seleccionarse un botón en el grupo, en un momento dado. • El método setFont (de la clase Component) establece el tipo de letra de un componente a un nuevo objeto de la clase Font (paquete java.awt). • 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 itemStateChanged. 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 son similares a los objetos JCheckBox en cuanto a que tienen dos estados: seleccionado y no seleccionado. Sin embargo, generalmente los botones de opción aparecen como un grupo, en el cual sólo puede seleccionarse un botón a la vez. Al seleccionar un botón de opción distinto, se obliga a los demás botones de opción a deseleccionarse. • Los objetos JRadioButton se utilizan para representar opciones mutuamente exclusivas. • La relación lógica entre los objetos JRadioButton se mantiene mediante un objeto ButtonGroup (paquete javax. swing). • El método add de ButtonGroup 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 11.10 JComboBox y el uso de una clase interna anónima para el manejo de eventos • Un objeto JComboBox proporciona una lista de elementos, de los cuales el usuario puede seleccionar uno. Los objetos JComboBox generan eventos ItemEvent. • Cada elemento en un objeto JComboBox tiene un índice. El primer elemento que se agrega a un objeto JComboBox aparece como el elemento actualmente seleccionado cuando se muestra el objeto JComboBox. Los otros elementos se seleccionan haciendo clic en el objeto JComboBox, el cual se expande en una lista, de la cual el usuario puede seleccionar un elemento. • El método setMaximumRowCount de JComboBox establece 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 que permite al usuario desplazarse por todos los elementos en la lista. • Una clase interna anónima es una forma especial de clase interna, que se declara sin un nombre y por lo general aparece dentro de la declaración de un método. 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. • El método getSelectedIndex de JcomboBox devuelve el índice del elemento seleccionado.

www.elsolucionario.net

530

Capítulo 11

Componentes de la GUI: parte 1

Sección 11.11 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 y de selección múltiple. • Cuando el usuario hace clic en un elemento de un objeto JList, se produce un evento ListSelectionEvent. El método addListSelectionListener registra un objeto ListSelectionListener para los eventos de selección de un objeto JList. Un objeto ListSelectionListener (paquete javax.swing.event) debe implementar el método valueChanged. • El método setVisibleRowCount de JList especifica el número de elementos visibles en la lista. • El método setSelectionMode de JList especifica el modo de selección de una lista. • 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, puede usarse un objeto JScrollPane para proporcionar la capacidad de desplazamiento. El método getContentPane de JFrame devuelve una referencia al panel de contenido de JFrame, en donde se muestran los componentes de la GUI. • El método getSelectedIndex de JList devuelve el índice del elemento seleccionado.

Sección 11.12 Listas de selección múltiple • Una lista de selección múltiple permite al usuario seleccionar muchos elementos de un objeto JList. • El método setFixedCellWidth de JList establece la anchura de un objeto JList. El método setFixedCellHeight establece la altura de cada elemento en un objeto JList. • No hay eventos para indicar que un usuario ha realizado varias selecciones en una lista de selección múltiple. Por lo general, un evento externo generado por otro componente de la GUI especifica cuándo deben procesarse las selecciones múltiples en un objeto JList. • El método setListData de JList establece los elementos a mostrar en un objeto JList. El método getSelectedValues de JList devuelve un arreglo de objetos Object que representan los elementos seleccionados en un objeto JList.

Sección 11.13 Manejo de eventos del ratón • Las interfaces de escucha de eventos MouseListener y MouseMotionListener se utilizan para manejar los eventos del ratón. Estos eventos se pueden atrapar para cualquier componente de la GUI que extienda a Component. • La interfaz MouseInputListener (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 como argumento. Un objeto MouseEvent contiene información acerca del evento de ratón que ocurrió, 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 (superclase de MouseEvent) permiten a una aplicación determinar cuál botón oprimió el usuario. • La interfaz MouseWheelListener permite a las aplicaciones responder a la rotación de la rueda de un ratón. • Los componentes de la GUI heredan los métodos addMouseListener y addMouseMotionListener de la clase Component.

Sección 11.14 Clases adaptadoras • Muchas interfaces de escucha de eventos contienen varios métodos. Para muchas de estas interfaces, 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 de cada método en la interfaz. Podemos extender una clase adaptadora para que herede la implementación predeterminada de cada método, y por consiguiente, podemos sobrescribir sólo el (los) método(s) necesario(s) para el manejo de eventos. • El método getClickCount de MouseEvent devuelve el número de clics de los botones del ratón. Los métodos isMetaDown e isAltDown determinan cuál botón del ratón oprimió el usuario.

Sección 11.15 Subclase de JPanel para dibujar con el ratón • Los componentes ligeros de Swing que extienden a la clase JComponent 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.

www.elsolucionario.net

Resumen

531

• Las subclases de JComponent soportan la transparencia. Cuando un componente es opaco, 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 (un argumento false indica que el componente es transparente). • La clase Point (paquete java.awt) representa una coordenada x-y. • La clase Graphics se utiliza para dibujar. • El método getPoint de MouseEvent obtiene el objeto Point en donde ocurrió un evento de ratón. • El método repaint (heredado directamente de la clase Component) indica que un componente debe actualizarse 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 componente ligero necesita mostrarse en la pantalla. • El método fillOval de Graphics dibuja un óvalo relleno. Los cuatro parámetros del método representan el cuadro delimitador en el cual se muestra el óvalo. Los primeros dos parámetros 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.

Sección 11.16 Manejo de eventos de teclas • La interfaz KeyListener se utiliza para manejar eventos de teclas, que se generan cuando se oprimen y sueltan las teclas en el teclado. El método addKeyListener de la clase Component registra un objeto KeyListener para un componente. • El método getKeyCode de KeyEvent obtiene el código de tecla virtual 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 de KeyEvent devuelve una cadena que contiene el nombre de la tecla que se oprimió. • El método getKeyChar de KeyEvent obtiene el valor Unicode del carácter escrito. • El método isActionKey de KeyEvent determina si la tecla en un evento fue una tecla de acción. • El método getModifiers de InputEvent determina si se oprimió alguna tecla modificadora (como Mayús, Alt y Ctrl ) cuando ocurrió el evento de tecla. • El método getKeyModifiersText de KeyEvent produce una cadena que contiene los nombres de las teclas modificadoras que se oprimieron.

Sección 11.17 Administradores de esquemas • Los administradores de esquemas ordenan los componentes de la GUI en un contenedor, para fines de presentación. • Todos los administradores de esquemas implementan la interfaz LayoutManager (paquete java.awt). • El método setLayout de la clase Container especifica el esquema de un contenedor. • FlowLayout es el administrador de esquemas más simple. Los componentes de la GUI se colocan en un contenedor, 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. • El método setAlignment de FlowLayout cambia la alineación para un objeto FlowLayout. • 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. • Un BorderLayout limita a un objeto Container para que contenga cuando mucho cinco componentes; uno en cada región. • El administrador de esquemas GridLayout divide el contenedor en una cuadrícula, de manera que los componentes puedan colocarse en filas y columnas. • 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.

Sección 11.19 JTextArea • Un objeto

JTextArea proporciona un área para manipular varias líneas de texto. JTextArea es una subclase de JTextComponent, la cual declara métodos comunes para objetos JTextField, JTextArea y varios otros compo-

nentes de GUI basados en texto. • La clase Box es una subclase de Container que utiliza un administrador de esquemas BoxLayout para ordenar los componentes de la GUI, ya sea en forma horizontal o vertical.

www.elsolucionario.net

532

Capítulo 11

Componentes de la GUI: parte 1

• 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. • El método getSelectedText (que hereda JTextArea de JTextComponent) devuelve el texto seleccionado de un objeto JTextArea. • Podemos establecer las políticas de las barras de desplazamiento horizontal y vertical de un objeto JScrollPane al momento de crearlo. Los métodos setHorizontalScrollBarPolicy y setVerticalScrollBarPolicy de JScrollPane pueden usarse para modificar las políticas de las barras de desplazamiento en cualquier momento.

Terminología AbstractButton, clase ActionEvent, clase ActionListener, interfaz actionPerformed, método de ActionListener add, método de Container add, método de la clase ButtonGroup addActionListener, método de la clase JTextField addItemListener, método de la clase AbstractButton addKeyListener, método de la clase Component addListSelectionListener, método de la clase JList addMouseListener, método de la clase Component addMouseMotionListener, método de la clase Component addWindowListener, método de la clase JFrame

administrador de esquemas apariencia visual área dedicada de dibujo AWTEvent, clase BorderLayout, clase Box, clase BoxLayout, clase ButtonGroup, clase capitalización tipo título de libro clase adaptadora clase adaptadora de escucha de eventos clase anidada clase de nivel superior clase interna clase interna anónima clase static anidada Class, clase Component, clase componente de escucha de eventos componente de GUI componente de GUI ligero componente de GUI pesado componentes de GUI de Swing constructor predeterminado de una clase interna anónima Container, clase controlado por eventos createHorizontalBox, método de la clase Box cuadro de diálogo despachar un evento

diálogo de entrada diálogo de mensaje enfoque escribir en un campo de texto EventListenerList, clase evento fillOval, método de la clase Graphics FlowLayout, clase Font, clase getActionCommand, método de ActionEvent getClass, método de Object getClickCount, método de MouseEvent getContentPane, método de JFrame getIcon, método de JLabel getKeyChar, método de KeyEvent getKeyCode, método de KeyEvent getKeyModifiersText, método de KeyEvent getKeyText, método de KeyEvent getModifiers, método de InputEvent getPassword, método de JPasswordField getPoint, método de MouseEvent getResource, método de Class getSelectedIndex, método de JComboBox getSelectedIndex, método de JList getSelectedText, método de JTextComponent getSelectedValues, método de JList getSource, método de EventObject getStateChange, método de ItemEvent getText, método de JLabel getX, método de MouseEvent getY, método de MouseEvent Graphics, clase GridLayout, clase Icon, interfaz icono de sustitución ImageIcon, clase información sobre herramientas InputEvent, clase interfaz de escucha de eventos interfaz gráfica de usuario (GUI) isActionKey, método de KeyEvent isAltDown, método de InputEvent isAltDown, método de MouseEvent isControlDown, método de InputEvent

www.elsolucionario.net

Ejercicios de autoevaluación isMetaDown, método de InputEvent isMetaDown, método de MouseEvent isSelected, método de JCheckBox isShiftDown, método de InputEvent ItemEvent, clase ItemListener, interfaz itemStateChanged, método de ItemListener java.awt, paquete java.awt.event, paquete javax.swing, paquete javax.swing.event, paquete JButton, clase JCheckBox, clase JComboBox, clase JComponent, clase JFrame, clase JLabel, clase JList, clase JOptionPane, clase JPanel, clase JPasswordField, clase JRadioButton, clase JScrollPane, clase JSlider, clase JTextArea, clase JTextComponent, clase JTextField, clase JToggleButton, clase KeyAdapter, clase KeyEvent, clase KeyListener, interfaz keyPressed, método de KeyListener keyReleased, método de KeyListener keyTyped, método de KeyListener layoutContainer, método de LayoutManager LayoutManager, interfaz LayoutManager2, interfaz listenerList, campo de JComponent ListSelectionEvent, clase ListSelectionListener, interfaz ListSelectionModel, clase

manejador de eventos manejo de eventos modelo de eventos por delegación MouseAdapter, clase mouseClicked, método de MouseListener mouseDragged, método de MouseMotionListener mouseEntered, método de MouseListener MouseEvent, clase mouseExited, método de MouseListener MouseInputListener, interfaz MouseListener, interfaz MouseMotionAdapter, clase

533

MouseMotionListener, interfaz mouseMoved, método de MouseMotionListener mousePressed, método de MouseListener mouseReleased, método de MouseListener MouseWheelEvent, clase MouseWheelListener, interfaz mouseWheelMoved, método de MouseWheelListener

objeto evento origen del evento paintComponent,

método de JComponent panel de contenido Point, clase registro de un evento registro de un manejador de eventos repaint, método de Component setAlignment, método de FlowLayout setBackground, método de Component setDefaultCloseOperation, método de JFrame setEditable, método de JTextComponent setFixedCellHeight, método de JList setFixedCellWidth, método de JList setFont, método de Component setHorizontalAlignment, método de JLabel setHorizontalScrollBarPolicy, método de JScrollPane setHorizontalTextPosition, método de JLabel setIcon, método de JLabel setLayout, método de Container setLineWrap, método de JTextArea setListData, método de JList setMaximumRowCount, método de JComboBox setOpaque, método de JComponent setRolloverIcon, método de AbstractButton setSelectionMode, método de JList setSize, método de JFrame setText, método de JLabel setText, método de JTextComponent setToolTipText, método de JComponent setVerticalAlignment, método de JLabel setVerticalScrollBarPolicy, método de JSlider setVerticalTextPosition, método de JLabel setVisible, método de Component setVisible, método de JFrame setVisibleRowCount, método de JList showInputDialog, método de JOptionPane showMessageDialog, método de JOptionPane SwingConstants, interfaz transparencia de un objeto JComponent validate, método de Container valueChanged, método de ListSelectionListener WindowAdapter, clase windowClosing, método de WindowListener WindowListener, interfaz

www.elsolucionario.net

534

Capítulo 11

Componentes de la GUI: parte 1

Ejercicios de autoevaluación 11.1

11.2

11.3

Complete las siguientes oraciones: a) El método __________________ es llamado cuando el ratón se mueve sin oprimir los botones y un componente de escucha 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 a la clase __________________. 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. d) En un esquema BorderLayout, dos botones que se agreguen a la región NORTH se mostrarán uno al lado del otro. e) Cuando se utiliza BorderLayout, sólo deben mostrarse un máximo de cinco componentes. 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. 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) Container contenedor = getContentPane(); 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ón 11.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. 11.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. f ) Falso. Las clases internas tienen acceso a todos los miembros de la declaración de la clase que las encierra.

www.elsolucionario.net

Ejercicios

535

g) Falso. Los objetos JTextArea pueden editarse de manera predeterminada. h) Falso. JTextArea se deriva de la clase JTextComponent. 11.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 );

Ejercicios 11.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.

11.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). 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.

11.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.

11.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

11.8

Cree la siguiente GUI. No tiene que proporcionar ningún tipo de funcionalidad.

11.9

Cree la siguiente GUI. No tiene que proporcionar ningún tipo de funcionalidad.

www.elsolucionario.net

536

Capítulo 11

Componentes de la GUI: parte 1

11.10 Cree la siguiente GUI. No tiene que proporcionar ningún tipo de funcionalidad.

11.11 Cree la siguiente GUI. No tiene que proporcionar ningún tipo de funcionalidad.

11.12 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: Celsius

=

5 --- ¥ ( Fahrenheit 9

– 32 )

11.13 Mejore la aplicación de conversión de temperatura del ejercicio 11.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órmula del ejercicio 11.12): Kelvin = Centígrados +

273.15

11.14 Escriba una aplicación que muestre los eventos según vayan ocurriendo en un objeto JTextArea. Proporcione un objeto JComboBox con un mínimo de cuatro elementos. El usuario deberá ser capaz de seleccionar del objeto JComboBox un evento a vigilar. Cuando ocurra ese evento específico, muestre información acerca del mismo en el objeto JTextArea. Use el método toString en el objeto evento para convertirlo en una representación de cadena. 11.15 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.

www.elsolucionario.net

Ejercicios

537

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. 11.16 A menudo es conveniente mostrar los eventos que ocurren durante la ejecución de un programa. 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 mostrarse en pantalla. El método toString crea un objeto String que contiene toda la información del objeto evento. 11.17 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 ejemplo práctico de GUI y gráficos: expansión de la interfaz 18.18 En este ejercicio, implementará una aplicación de GUI que utiliza la jerarquía MiFigura del ejercicio 10.2 del ejemplo práctico de GUI, 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. e) Un objeto Color llamado colorActual, que represente el color del dibujo actual. f ) Una variable bolean 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 instancia 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 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.

www.elsolucionario.net

538

Capítulo 11

Componentes de la GUI: parte 1

Sobrescriba el método mouseMoved para establecer el texto de etiquetaEstado, de manera que muestre las coordenadas 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 usuario 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 etiquetaEstado 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 componentes 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 11.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 constructor de PanelDibujo. Por último, cree una clase de prueba para inicializar y mostrar el objeto Marco-Dibujo para ejecutar la aplicación.

Figura 11.49 | Interfaz para dibujar figuras.

www.elsolucionario.net

12 Gráficos y Java 2D™ Una 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

OBJETIVOS En este capítulo aprenderá a: Q

Comprender los contextos y los objetos de gráficos.

Q

Entender y manipular los colores.

Q

Comprender y manipular las fuentes.

Q

Usar métodos de la clase Graphics para dibujar líneas, rectángulos, rectángulos con esquinas redondeadas, rectángulos tridimensionales, óvalos, arcos y polígonos.

Q

Utilizar métodos de la clase Graphics2D de la API Java 2D para dibujar líneas, rectángulos, rectángulos con esquinas redondeadas, elipses, arcos y rutas en general.

Q

Especificar las características Paint y Stroke de las figuras mostradas con Graphics2D.

www.elsolucionario.net

Pla n g e ne r a l

540

Capítulo 12

12.1 12.2 12.3 12.4 12.5 12.6 12.7 12.8 12.9

Gráficos y Java 2D™

Introducción Contextos y objetos de gráficos Control de colores Control de tipos de letra Dibujo de líneas, rectángulos y óvalos Dibujo de arcos Dibujo de polígonos y polilíneas La API Java 2D Conclusión

Resumen | Terminología | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

12.1 Introducción En este capítulo veremos varias de las herramientas de Java para dibujar figuras bidimensionales, controlar 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. [Nota: ya hemos cubierto varios de los conceptos de este capítulo en el ejemplo práctico opcional de GUI y gráficos de los capítulos 3 a 10. Por lo tanto, parte del material será repetitivo si usted leyó el ejemplo práctico; sin embargo, si no lo ha leído, nos es necesario para comprender este capítulo]. En la figura 12.1 se muestra una porción 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 Java2 que cubriremos en este capítulo. La clase Color contiene métodos y constantes para manipular los colores. La clase JComponent contiene 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étodos para obtener información sobre los 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 TexturePaint ayudan a especificar las características para rellenar figuras con colores o patrones. Las clases GeneralPath, Line2D, Arc2D, Ellipse2D, Rectangle2D y RoundRectangle2D representan varias figuras de Java 2D. [Nota: empezaremos el capítulo hablando sobre las herramientas de gráficos originales de Java, y después pasaremos a la API Java 2D. Ahora, las clases que formaron parte de las herramientas de gráficos originales de Java se consideran parte de la API Java 2D]. Para empezar a dibujar en Java, primero debemos entender su sistema de coordenadas (figura 12.2), el cual es un esquema para identificar a cada uno de los posibles puntos en la pantalla. De manera 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 coordenada horizontal) y una coordenada y (la coordenada vertical). La coordenada x es la distancia horizontal 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 unidades de las coordenadas se miden en píxeles (“elementos de imagen”). Un píxel es la unidad más pequeña de resolución de un monitor de computadora.

Tip de portabilidad 12.1 Existen 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.

www.elsolucionario.net

12.1 Introducción

java.lang.Object

java.awt.Color

java.awt.Component

java.awt.Container

javax.swing.JComponent

java.awt.Font

java.awt.FontMetrics

java.awt.Graphics

java.awt.Graphics2D

java.awt.Polygon «interfaz» java.awt.Paint java.awt.BasicStroke

java.awt.GradientPaint

java.awt.TexturePaint

«interfaz» java.awt.Shape

«interfaz» java.awt.Stroke

java.awt.geom.GeneralPath

java.awt.geom.Line2D

java.awt.geom.RectangularShape

java.awt.geom.Arc2D

java.awt.geom.Ellipse2D

java.awt.geom.Rectangle2D

java.awt.geom.RoundRectangle2D

Figura 12.1 | Clases e interfaces utilizadas en este capítulo, provenientes de las herramientas de gráficos originales de Java y de la API Java2D. [Nota: la clase Object aparece aquí debido a que es la superclase de la jerarquía de clases de Java. Además, las clases abstract aparecen en cursiva].

www.elsolucionario.net

541

542

Capítulo 12

Gráficos y Java 2D™

+x

(0, 0)

eje x

(x,y)

+y eje y

Figura 12.2 | Sistema de coordenadas de Java. Las unidades se miden en píxeles.

12.2 Contextos y objetos de gráficos Un 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, manipular 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 plataforma 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. La clase Component es la superclase para muchas de las clases en el paquete java.awt. (En el capítulo 11 presentamos la clase Component). La clase JComponent, que hereda directamente de 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 utilizamos en algunas de las aplicaciones del capítulo 11. 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. Cuando se ejecuta una aplicación de GUI, el contenedor de la aplicación llama al método paintComponent 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 todos los objetos JComponent heredan indirectamente de la clase Component (paquete java.awt). El método repaint se llama con frecuencia para solicitar una llamada al método paintComponent. El encabezado para repaint es: public void repaint()

12.3 Control de colores La 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 12.3, y varios métodos y constructores para los

www.elsolucionario.net

12.3 Control de colores

543

colores se sintetizan en la figura 12.4. Observe que dos de los métodos de la figura 12.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

Figura 12.3 | Constantes de Color y sus valores RGB.

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 flotante de 0.0 a 1.0. 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áficos. public void setColor( Color c )

Establece el color actual para dibujar con el contexto de gráficos.

Figura 12.4 | Los métodos de Color y los métodos de Graphics relacionados con los colores.

www.elsolucionario.net

544

Capítulo 12

Gráficos 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 seleccionar de entre 256 x 256 x 256 (o aproximadamente 16.7 millones de) colores. No todas las computadoras son capaces de mostrar todos estos colores. La computadora mostrará el color más cercano que pueda. En la figura 12.4 se muestran dos de los constructores de la clase Color (uno que toma tres argumentos 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, azul y verde que se especifiquen. Los métodos getRed, getGreen y getBlue de Color devuelven valores enteros de 0 a 255, los cuales representan 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. Las figuras 12.5 y 12.6 demuestran varios métodos de la figura 12.4, al dibujar rectángulos rellenos y cadenas 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 12.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 azul y verde). 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 primeros 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 positivos que representan la anchura y la altura del rectángulo en píxeles, respectivamente. Un rectángulo que se dibuja usando el método fillRect se rellena con el color actual del objeto Graphics. En la línea 19 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. El objeto Color devuelto se concatena 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.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

// Fig. 12.5: JPanelColor.java // Demostración de objetos Color. import java.awt.Graphics; import java.awt.Color; import javax.swing.JPanel; public class JPanelColor extends JPanel { // dibuja rectángulos y objetos String en distintos colores public void paintComponent( Graphics g ) { super.paintComponent( g ); // llama al método paintComponent de la superclase this.setBackground( Color.WHITE ); // establece nuevo color de dibujo, usando valores enteros g.setColor( new Color( 255, 0, 0 ) ); g.fillRect( 15, 25, 100, 20 ); g.drawString( "RGB actual: " + g.getColor(), 130, 40 ); // establece nuevo color de dibujo, usando valores de punto flotante g.setColor( new Color( 0.50f, 0.75f, 0.0f ) );

Figura 12.5 | Programa para imprimir texto. (Parte 1 de 2).

www.elsolucionario.net

12.3 Control de colores

23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38

545

g.fillRect( 15, 50, 100, 20 ); g.drawString( "RGB actual: " + g.getColor(), 130, 65 ); // establece nuevo color de dibujo, usando objetos Color static g.setColor( Color.BLUE ); g.fillRect( 15, 75, 100, 20 ); g.drawString( "RGB actual: " + g.getColor(), 130, 90 ); // muestra los valores RGB individuales Color color = Color.MAGENTA; g.setColor( color ); g.fillRect( 15, 100, 100, 20 ); g.drawString( "Valores RGB: " + color.getRed() + ", " + color.getGreen() + ", " + color.getBlue(), 130, 115 ); } // fin del método paintComponent } // fin de la clase JPanelColor

Figura 12.5 | Cambio de colores para dibujar. (Parte 2 de 2).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

// Fig. 12.6: MostrarColores.java // Demostración de objetos Color. import javax.swing.JFrame; public class MostrarColores { // ejecuta la aplicación public static void main( String args[] ) { // crea marco para objeto JPanelColor JFrame frame = new JFrame( "Uso de colores" ); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); JPanelColor jPanelColor = new JPanelColor(); // crea objeto JPanelColor frame.add( jPanelColor ); // agrega jPanelColor a marco frame.setSize( 400, 180 ); // establece el tamaño del marco frame.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase MostrarColores

Figura 12.6 | Creación de un objeto JFrame para mostrar colores en un objeto JPanel.

Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 12.1 Todos 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.

www.elsolucionario.net

546

Capítulo 12

Gráficos y Java 2D™

En las líneas 22 a 24 y 27 a 29 se llevan a cabo, nuevamente, las mismas tareas. En la línea 22 se utiliza el constructor de Color con tres argumentos float para crear el 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. De manera predeterminada, 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 previamente declaradas (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 previamente declarada. El método main de la clase MostrarColores (líneas 8 a 18 de la figura 12.6) crea el objeto JFrame que contendrá un objeto ColorJPanel, en donde se mostrarán los colores.

Observación de ingeniería de software 12.1 Para 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 para permitir a los usuarios de aplicaciones seleccionar colores. La aplicación de las figuras 12.7 y 12.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, el color de fondo de la ventana de la aplicación cambia.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

// Fig. 12.7: MostrarColores2JFrame.java // Selección de colores con JColorChooser. import java.awt.BorderLayout; import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JColorChooser; import javax.swing.JPanel; public class MostrarColores2JFrame extends JFrame { private JButton cambiarColorJButton; private Color color = Color.LIGHT_GRAY; private JPanel coloresJPanel; // establece la GUI public MostrarColores2JFrame() { super( "Uso de JColorChooser" ); // crea objeto JPanel para mostrar color coloresJPanel = new JPanel(); coloresJPanel.setBackground( color ); // establece cambiarColorJButton y registra su manejador de eventos cambiarColorJButton = new JButton( "Cambiar color" ); cambiarColorJButton.addActionListener( new ActionListener() // clase interna anónima { // muestra JColorChooser cuando el usuario hace clic con el botón

Figura 12.7 | Cuadro de diálogo JColorChooser. (Parte 1 de 2).

www.elsolucionario.net

12.3 Control de colores

34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55

547

public void actionPerformed( ActionEvent evento ) { color = JColorChooser.showDialog( MostrarColores2JFrame.this, "Seleccione un color", color ); // establece el color predeterminado, si no se devuelve un color if ( color == null ) color = Color.LIGHT_GRAY; // cambia el color de fondo del panel de contenido coloresJPanel.setBackground( color ); } // fin del método actionPerformed } // fin de la clase interna anónima ); // fin de la llamada a addActionListener add( coloresJPanel, BorderLayout.CENTER ); // agrega coloresJPanel add( cambiarColorJButton, BorderLayout.SOUTH ); // agrega botón setSize( 400, 130 ); // establece el tamaño del marco setVisible( true ); // muestra el marco } // fin del constructor de MostrarColores2JFrame } // fin de la clase MostrarColores2JFrame

Figura 12.7 | Cuadro de diálogo JColorChooser. (Parte 2 de 2). La clase JColorChooser proporciona el método estático showDialog, el cual crea un objeto JColorChoolo adjunta a un cuadro de diálogo y lo muestra en pantalla. Las líneas 36 y 37 de la figura 12.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 cuadro de diálogo (en este caso el objeto JFrame, con el nombre de referencia marco). Este cuadro de 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 cuadro de diálogo para seleccionar colores se encuentre en la pantalla, el usuario no podrá interactuar con el componente padre. A este tipo de cuadro de diálogo se le conoce como cuadro de diálogo modal (el cual se describirá en el capítulo 22, Componentes de la GUI: parte 2). 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. Observe que el usuario puede seguir utilizando el botón Cambiar color para cambiar el color de fondo de la aplicación. La figura 12.8 contiene el método main, que ejecuta el programa. ser,

1 2 3 4 5 6 7 8 9

// Fig. 12.8: MostrarColores2.java // Selección de colores con JColorChooser. import javax.swing.JFrame; public class MostrarColores2 { // ejecuta la aplicación public static void main( String args[] ) {

Figura 12.8 | Selección de colores con JColorChooser. (Parte 1 de 2).

www.elsolucionario.net

548

10 11 12 13

Capítulo 12

Gráficos y Java 2D™

MostrarColores2JFrame aplicacion = new MostrarColores2JFrame(); aplicacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); } // fin de main } // fin de la clase MostrarColores2

Seleccione un color de una de las muestras de colores

Figura 12.8 | Selección de colores con JColorChooser. (Parte 2 de 2).

La segunda captura de pantalla de la figura 12.8 muestra el cuadro de diálogo JColorChooser predeterminado, que permite al usuario seleccionar un color de una variedad de muestras de colores. Observe que en realidad hay tres fichas en la parte superior 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 matiz, saturación y brillo, visite whatis.techtarget.com/definition/0,,sid9_gci212262,00.html. 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 12.9.

12.4 Control de tipos de letra En 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 12.10.

www.elsolucionario.net

12.4 Control de tipos de letra

Controles deslizables para seleccionar los componentes rojo, verde y azul

Figura 12.9 | Las fichas HSB y RGB del cuadro de diálogo JColorChooser.

Método o constante

Descripción

Constantes, constructores y métodos de 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 especificados.

public int getStyle()

Devuelve un valor entero que indica el estilo actual de tipo de letra.

public int getSize()

Devuelve un valor entero que indica el tamaño actual del tipo de letra.

Figura 12.10 | Métodos y constantes relacionados con Font. (Parte 1 de 2).

www.elsolucionario.net

549

550

Capítulo 12

Gráficos y Java 2D™

Método o constante

Descripción

Constantes, constructores y métodos de Font 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.

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 especificados por la referencia f al objeto Font.

Figura 12.10 | Métodos y constantes relacionados con Font. (Parte 2 de 2). 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 momento (el tipo de letra en el cual se mostrará el texto) en base a su argumento Font.

Tip de portabilidad 12.2 El 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 12.11 y 12.12 muestra texto en cuatro tipos de letra distintos, con cada tipo de letra en diferente tamaño. La figura 12.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 1 2 3 4 5 6 7 8 9 10

// Fig. 12.11: FontJPanel.java // Muestra cadenas en distintos tipos de letra y colores. import java.awt.Font; import java.awt.Color; import java.awt.Graphics; import javax.swing.JPanel; public class FontJPanel extends JPanel { // muestra objetos String en distintos tipos de letra y colores

Figura 12.11 | El método setFont de Graphics cambia el tipo de letra para dibujar. (Parte 1 de 2).

www.elsolucionario.net

12.4 Control de tipos de letra

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

public void paintComponent( Graphics g ) { super.paintComponent( g ); // llama al método paintComponent de la superclase // establece el tipo de letra a Serif (Times), negrita, 12 puntos y dibuja una cadena g.setFont( new Font( "Serif", Font.BOLD, 12 ) ); g.drawString( "Serif 12 puntos, negrita.", 20, 50 ); // establece el tipo de letra a Monospaced (Courier), cursiva, 24 puntos y dibuja una cadena g.setFont( new Font( "Monospaced", Font.ITALIC, 24 ) ); g.drawString( "Monospaced 24 puntos, cursiva.", 20, 70 ); // establece el tipo de letra a SansSerif (Helvetica), simple, 14 puntos y dibuja una cadena g.setFont( new Font( "SansSerif", Font.PLAIN, 14 ) ); g.drawString( "SansSerif 14 puntos, simple.", 20, 90 ); // establece el tipo de letra a Serif (Times), negrita/cursiva, 18 puntos y dibuja una cadena g.setColor( Color.RED ); g.setFont( new Font( "Serif", Font.BOLD + Font.ITALIC, 18 ) ); g.drawString( g.getFont().getName() + " " + g.getFont().getSize() + " puntos, negrita cursiva.", 20, 110 ); } // fin del método paintComponent } // fin de la clase FontJPanel

Figura 12.11 | El método setFont de Graphics cambia el tipo de letra para dibujar. (Parte 2 de 2).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

551

// Fig. 12.12: TiposDeLetra.java // Uso de tipos de letra. import javax.swing.JFrame; public class TiposDeLetra { // ejecuta la aplicación public static void main( String args[] ) { // crea marco para FontJPanel JFrame marco = new JFrame( “Uso de tipos de letra” ); marco.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); FontJPanel fontJPanel = new FontJPanel(); // crea objeto FontJPanel marco.add( fontJPanel ); // agrega objeto fontJPanel al marco marco.setSize( 475, 170 ); // establece el tamaño del marco marco.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase TiposDeLetra

Figura 12.12 | Creación de un objeto JFrame para mostrar tipos de letra.

www.elsolucionario.net

552

Capítulo 12

Gráficos y Java 2D™

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. Observe que la coordenada que se pasa a drawString corresponde 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 a 31 se muestra informació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. La figura 12.12 contiene el método main, que crea un objeto JFrame. Agregamos un objeto FontJPanel a este objeto JFrame (línea 15), el cual muestra los gráficos creados en la figura 12.11.

Observación de ingeniería de software 12.2 Para 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.

Métrica de los tipos de letra En ocasiones es necesario obtener información acerca del tipo de letra actual para dibujar, como el nombre, el estilo y el tamaño del tipo de letra. En la figura 12.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 plataforma. 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 12.10. Los métodos isPlain, isBold e isItalic devuelven true si el estilo del tipo de letra actual es simple, negrita o cursiva, respectivamente. Algunas veces es necesario conocer información precisa acerca de la métrica de un tipo de letra, como la altura, el descendente (la distancia 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 descendente de una línea de texto y el ascendente de la línea de texto que está debajo; es decir, el espaciamiento entre líneas). En la figura 12.13 se muestran algunos elementos comunes de la métrica de los tipos de letras. La clase FontMetrics declara varios métodos para obtener información métrica de los tipos de letra. En la figura 12.14 se sintetizan estos métodos, junto con el método getFontMetrics de la clase Graphics. La aplicación de las figuras 12.15 y 12.16 utiliza los métodos de la figura 12.14 para obtener la información métrica de dos tipos de letra. En la línea 15 de la figura 12.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 objeto 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 devuelve 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.

interlineado

altura

ascendente línea base descendente

Figura 12.13 | Métrica de los tipos de letra.

www.elsolucionario.net

12.4 Control de tipos de letra

Método

553

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 especificado.

Figura 12.14 | Métodos de FontMetrics y Graphics para obtener la métrica de los tipos de letra.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

// Fig. 12.15: MetricaJPanel.java // Métodos de FontMetrics y Graphics útiles para obtener la métrica de los tipos de letra. import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import javax.swing.JPanel; public class MetricaJPanel extends JPanel { // muestra la métrica de los tipos de letra public void paintComponent( Graphics g ) { super.paintComponent( g ); // llama al método paintComponent de la superclase g.setFont( new Font( "SansSerif", Font.BOLD, 12 ) ); FontMetrics metrica = g.getFontMetrics(); g.drawString( "Tipo de letra actual: " + g.getFont(), 10, 40 ); g.drawString( "Ascendente: " + metrica.getAscent(), 10, 55 ); g.drawString( "Descendente: " + metrica.getDescent(), 10, 70 ); g.drawString( "Altura: " + metrica.getHeight(), 10, 85 ); g.drawString( "Interlineado: " + metrica.getLeading(), 10, 100 ); Font tipoLetra = new Font( "Serif", Font.ITALIC, 14 ); metrica = g.getFontMetrics( tipoLetra ); g.setFont( tipoLetra ); g.drawString( "Tipo de letra actual: " + tipoLetra, 10, 130 ); g.drawString( "Ascendente: " + metrica.getAscent(), 10, 145 ); g.drawString( "Descendente: " + metrica.getDescent(), 10, 160 ); g.drawString( "Altura: " + metrica.getHeight(), 10, 175 ); g.drawString( "Interlineado: " + metrica.getLeading(), 10, 190 ); } // fin del método paintComponent } // fin de la clase MetricaJPanel

Figura 12.15 | Métrica de los tipos de letra.

www.elsolucionario.net

554

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

Capítulo 12

Gráficos y Java 2D™

// Fig. 12.16: Metrica.java // Muestra la métrica de los tipos de letra. import javax.swing.JFrame; public class Metrica { // ejecuta la aplicación public static void main( String args[] ) { // crea marco para objeto MetricaJPanel JFrame marco = new JFrame( “Demostracion de FontMetrics” ); marco.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); MetricaJPanel metricaJPanel = new MetricaJPanel(); marco.add( metricaJPanel ); // agrega metricaJPanel al marco marco.setSize( 530, 250 ); // establece el tamaño del marco marco.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase Metrica

Figura 12.16 | Creación de un objeto JFrame para mostrar información sobre la métrica de los tipos de letra.

12.5 Dibujo de líneas, rectángulos y óvalos En 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 12.17. Para cada método de dibujo que requiere un parámetro anchura y otro altura, sus valores deben ser números no negativos. De lo contrario, no se mostrará la figura.

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 especificadas. 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 especificadas. La esquina superior izquierda del rectángulo tiene las coordenadas (x, y). El rectángulo se rellena con el color del objeto Graphics.

Figura 12.17 | Métodos de Graphics para dibujar líneas, rectángulos y óvalos. (Parte 1 de 2).

www.elsolucionario.net

12.5

Método

Dibujo de líneas, rectángulos y óvalos

555

Descripción

public void clearRect( int x, int y, int anchura, int altura )

Dibuja un rectángulo relleno con la anchura y altura especificadas, 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 especificadas. Los valores de anchuraArco y alturaArco determinan el grado de redondez de las esquinas (vea la figura 12.20). Sólo se dibuja el contorno de la figura. 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 especificadas. Los valores de anchuraArco y alturaArco determinan el grado de redondez de las esquinas (vea la figura 12.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 especificadas. 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 figura. 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 especificadas. 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 especificadas. 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 figura 12.21). Sólo se dibuja el contorno de la figura. public void fillOval( int x, int y, int anchura, int altura )

Dibuja un óvalo relleno en el color actual, con la anchura y altura especificadas. 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 figura 12.21).

Figura 12.17 | Métodos de Graphics para dibujar líneas, rectángulos y óvalos. (Parte 2 de 2). La aplicación de las figuras 12.18 y 12.19 demuestra cómo dibujar una variedad de líneas, rectángulos, rectángulos tridimensionales, rectángulos con esquinas redondeadas y óvalos. En la figura 12.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). Observe que 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 cuadradas. Los argumentos tercero y cuarto especifican la anchura y altura del rectángulo. Sus últimos dos argumentos determinan los diámetros vertical y horizontal del arco (es decir, la anchura y la altura del arco) que se utiliza para representar las esquinas.

www.elsolucionario.net

556

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

Capítulo 12

Gráficos y Java 2D™

// Fig. 12.18: LineasRectsOvalosJPanel.java // Dibujo de líneas, rectángulos y óvalos. import java.awt.Color; import java.awt.Graphics; import javax.swing.JPanel; public class LineasRectsOvalosJPanel extends JPanel { // muestra varias líneas, rectángulos y óvalos public void paintComponent( Graphics g ) { super.paintComponent( g ); // llama al método paintComponent de la superclase this.setBackground( Color.WHITE ); g.setColor( Color.RED ); g.drawLine( 5, 30, 380, 30 ); g.setColor( Color.BLUE ); g.drawRect( 5, 40, 90, 55 ); g.fillRect( 100, 40, 90, 55 ); g.setColor( Color.CYAN ); g.fillRoundRect( 195, 40, 90, 55, 50, 50 ); g.drawRoundRect( 290, 40, 90, 55, 20, 20 ); g.setColor( Color.YELLOW ); g.draw3DRect( 5, 100, 90, 55, true ); g.fill3DRect( 100, 100, 90, 55, false ); g.setColor( Color.MAGENTA ); g.drawOval( 195, 100, 90, 55 ); g.fillOval( 290, 100, 90, 55 ); } // fin del método paintComponent } // fin de la clase LineasRectsOvalosJPanel

Figura 12.18 | Dibujo de líneas, rectángulos y óvalos.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

// Fig. 12.19: LineasRectsOvalos.java // Dibujo de líneas, rectángulos y óvalos. import java.awt.Color; import javax.swing.JFrame; public class LineasRectsOvalos { // ejecuta la aplicación public static void main( String args[] ) { // crea marco para LineasRectsOvalosJPanel JFrame marco = new JFrame( "Dibujo de lineas, rectangulos y ovalos" ); marco.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); LineasRectsOvalosJPanel lineasRectsOvalosJPanel = new LineasRectsOvalosJPanel(); lineasRectsOvalosJPanel.setBackground( Color.WHITE ); marco.add( lineasRectsOvalosJPanel ); // agrega el panel al marco

Figura 12.19 | Creación de JFrame para mostrar líneas, rectángulos y óvalos. (Parte 1 de 2).

www.elsolucionario.net

12.5

20 21 22 23

Dibujo de líneas, rectángulos y óvalos

557

marco.setSize( 400, 210 ); // establece el tamaño del marco marco.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase LineasRectsOvalos

fillRoundRect

drawLine

drawRoundRect

drawRect

drawOval

fillRect draw3DRect

fillOval

fill3DRect

Figura 12.19 | Creación de JFrame para mostrar líneas, rectángulos y óvalos. (Parte 2 de 2).

En la figura 12.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 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 siguientes dos argumentos especifican la anchura y altura del rectángulo, respectivamente. El último argumento determina si el rectángulo está con relieve (true) o sin relieve (false). El efecto tridimensional 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 ligeramente 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 (líneas 32 y 33) reciben los mismos cuatro argumentos. 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 12.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).

(x, y)

altura del arco anchura del arco altura

anchura

Figura 12.20 | Anchura y altura del arco para los rectángulos redondeados.

www.elsolucionario.net

558

Capítulo 12

Gráficos y Java 2D™

(x,y)

altura

anchura

Figura 12.21 | Óvalo delimitado por un rectángulo.

12.6 Dibujo de arcos Un 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 12.22 se muestran dos arcos. El conjunto izquierdo de ejes muestra a un arco extendiéndose desde cero hasta aproximadamente 110 grados. Los arcos que se extienden 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 negativos. Observe los cuadros punteados alrededor de los arcos en la figura 12.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 sintetizan en la figura 12.23.

Ángulos positivos 90º

180º

Ángulos negativos 90º



180º

270º



270º

Figura 12.22 | Ángulos positivos y negativos de un arco.

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 especificadas. El segmento del arco se dibuja empezando en anguloInicial y se extiende hasta los grados especificados 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 especificadas. El segmento del arco se dibuja empezando en anguloInicial y se extiende hasta los grados especificados por anguloArco.

Figura 12.23 | Métodos de Graphics para dibujar arcos.

www.elsolucionario.net

12.6 Dibujo de arcos

559

La aplicación de las figuras 12.24 y 12.25 demuestra el uso de los métodos para arcos de la figura 12.23. La aplicació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ángulo amarillo que tiene los mismos argumentos x, y, anchura y altura que los arcos.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

// Fig. 12.24: ArcosJPanel.java // Dibujo de arcos. import java.awt.Color; import java.awt.Graphics; import javax.swing.JPanel; public class ArcosJPanel extends JPanel { // dibuja rectángulos y arcos public void paintComponent( Graphics g ) { super.paintComponent( g ); // llama al método paintComponent de la superclase // empieza en 0 y se extiende hasta 360 grados g.setColor( Color.RED ); g.drawRect( 15, 35, 80, 80 ); g.setColor( Color.BLACK ); g.drawArc( 15, 35, 80, 80, 0, 360 ); // empieza en 0 y se extiende hasta 110 g.setColor( Color.RED ); g.drawRect( 100, 35, 80, 80 ); g.setColor( Color.BLACK ); g.drawArc( 100, 35, 80, 80, 0, 110 ); // empieza en 0 y se extiende hasta -270 grados g.setColor( Color.RED ); g.drawRect( 185, 35, 80, 80 ); g.setColor( Color.BLACK ); g.drawArc( 185, 35, 80, 80, 0, -270 ); // empieza en 0 y se extiende hasta 360 grados g.fillArc( 15, 120, 80, 40, 0, 360 ); // empieza en 270 y se extiende hasta -90 grados g.fillArc( 100, 120, 80, 40, 270, -90 ); // empieza en 0 y se extiende hasta -270 grados g.fillArc( 185, 120, 80, 40, 0, -270 ); } // fin del método paintComponent } // fin de la clase ArcosJPanel

Figura 12.24 | Arcos mostrados con drawArc y

1 2 3 4 5 6

fillArc.

// Fig. 12.25: DibujarArcos.java // Dibujo de arcos. import javax.swing.JFrame; public class DibujarArcos {

Figura 12.25 | Creación de un objeto JFrame para mostrar arcos. (Parte 1 de 2).

www.elsolucionario.net

560

7 8 9 10 11 12 13 14 15 16 17 18 19

Capítulo 12

Gráficos y Java 2D™

// ejecuta la aplicación public static void main( String args[] ) { // crea marco para ArcosJPanel JFrame marco = new JFrame( "Dibujo de arcos" ); marco.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); ArcosJPanel arcosJPanel = new ArcosJPanel(); // crea objeto ArcosJPanel marco.add( arcosJPanel ); // agrega arcosJPanel al marco marco.setSize( 300, 210 ); // establece el tamaño del marco marco.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase DibujarArcos

Figura 12.25 | Creación de un objeto JFrame para mostrar arcos. (Parte 2 de 2).

12.7 Dibujo de polígonos y polilíneas Los polígonos son figuras cerradas de varios lados, compuestas por segmentos de línea recta. Las polilíneas son una secuencia de puntos conectados. En la figura 12.26 describimos los métodos para dibujar polígonos y polilíneas. Observe que algunos métodos requieren un objeto Polygon (paquete java.awt). Los constructores de la clase Polygon se describen también en la figura 12.26. La aplicación de las figuras 12.27 y 12.28 dibuja polígonos y polilíneas.

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 especifica en el arreglo puntosX y la coordenada y de cada punto se especifica en el arreglo puntosY. El último argumento especifica 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 especifica en el arreglo puntosX y la coordenada y de cada punto se especifica en el arreglo puntosY. El último argumento especifica el número de puntos. Si el último punto es distinto del primero, la polilínea no se cierra.

Figura 12.26 | Métodos de Graphics para dibujar polígonos y métodos de la clase Polygon. (Parte 1 de 2).

www.elsolucionario.net

12.7

Método

Dibujo de polígonos y polilíneas

561

Descripción

public void drawPolygon( Polygon p )

Dibuja el polígono especificado. public void fillPolygon( int puntosX[], int puntosY[], int puntos )

Dibuja un polígono relleno. La coordenada x de cada punto se especifica en el arreglo puntosX y la coordenada y de cada punto se especifica en el arreglo puntosY. El último argumento especifica 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 especificado. El polígono es cerrado. 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.

Figura 12.26 | Métodos de Graphics para dibujar polígonos y métodos de la clase Polygon. (Parte 2 de 2).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

// Fig. 12.27: PoligonosJPanel.java // Dibujo de polígonos. import java.awt.Graphics; import java.awt.Polygon; import javax.swing.JPanel; public class PoligonosJPanel extends JPanel { // dibuja polígonos y polilíneas public void paintComponent( Graphics g ) { super.paintComponent( g ); // llama al método paintComponent de la superclase // dibuja polígono con objeto polígono int valoresX[] = { 20, 40, 50, 30, 20, 15 }; int valoresY[] = { 50, 50, 60, 80, 80, 60 }; Polygon poligono1 = new Polygon( valoresX, valoresY, 6 ); g.drawPolygon( poligono1 ); // dibuja polilíneas con dos arreglos int valoresX2[] = { 70, 90, 100, 80, 70, 65, 60 };

Figura 12.27 | Polígonos mostrados con drawPolygon y fillPolygon. (Parte 1 de 2).

www.elsolucionario.net

562

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

Capítulo 12

Gráficos y Java 2D™

int valoresY2[] = { 100, 100, 110, 110, 130, 110, 90 }; g.drawPolyline( valoresX2, valoresY2, 7 ); // rellena polígono con dos arreglos int valoresX3[] = { 120, 140, 150, 190 }; int valoresY3[] = { 40, 70, 80, 60 }; g.fillPolygon( valoresX3, valoresY3, 4 ); // dibuja polígono relleno con objeto Polygon Polygon poligono2= new Polygon(); poligono2.addPoint( 165, 135 ); poligono2.addPoint( 175, 150 ); poligono2.addPoint( 270, 200 ); poligono2.addPoint( 200, 220 ); poligono2.addPoint( 130, 180 ); g.fillPolygon( poligono2 ); } // fin del método paintComponent } // fin de la clase PoligonosJPanel

Figura 12.27 | Polígonos mostrados con drawPolygon y fillPolygon. (Parte 2 de 2).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

// Fig. 12.28: DibujarPoligonos.java // Dibujo de polígonos. import javax.swing.JFrame; public class DibujarPoligonos { // ejecuta la aplicación public static void main( String args[] ) { // crea marco para objeto PoligonosJPanel JFrame marco = new JFrame( "Dibujo de poligonos" ); marco.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); PoligonosJPanel poligonosJPanel = new PoligonosJPanel(); marco.add( poligonosJPanel ); // agrega poligonosJPanel al marco marco.setSize( 280, 270 ); // establece el tamaño del marco marco.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase DibujarPoligonos

Resultado de la línea 28 Resultado de la línea 18 Resultado de la línea 37 Resultado de la línea 23

Figura 12.28 | Creación de un objeto JFrame para mostrar polígonos.

www.elsolucionario.net

12.8

La API Java 2D

563

En las líneas 15 y 16 de la figura 12.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 recibe 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 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 12.1 Se lanzará una excepción ArrayIndexOutOfBoundsException si el número de puntos especificados 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 utiliza 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.

12.8 La API Java 2D La API Java 2D proporciona herramientas avanzadas para gráficos bidimensionales, para los programadores 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 20, Introducción a las applets de Java) o visite la página Web java.sun. com/products/java-media/2D/index.html. 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 herramientas 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 elipses En 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 de punto flotante con doble precisión. Hay una versión separada de cada figura, representada con valores de punto flotante con precisión simple (como Ellipse2D.Float). En cada caso, Double es una clase static anidada 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 12.29 y 12.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.

www.elsolucionario.net

564

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

Capítulo 12

Gráficos y Java 2D™

// Fig. 12.29: FigurasJPanel.java // Demostración de algunas figuras de Java 2D. import java.awt.Color; import java.awt.Graphics; import java.awt.BasicStroke; import java.awt.GradientPaint; import java.awt.TexturePaint; import java.awt.Rectangle; import java.awt.Graphics2D; import java.awt.geom.Ellipse2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import java.awt.geom.Arc2D; import java.awt.geom.Line2D; import java.awt.image.BufferedImage; import javax.swing.JPanel; public class FigurasJPanel extends JPanel { // dibuja figuras con la API Java 2D public void paintComponent( Graphics g ) { super.paintComponent( g ); // llama al método paintComponent de la superclase Graphics2D g2d = ( Graphics2D ) g; // convierte a g en objeto Graphics2D // dibuja un elipse en 2D, relleno con un gradiente color azul-amarillo g2d.setPaint( new GradientPaint( 5, 30, Color.BLUE, 35, 100, Color.YELLOW, true ) ); g2d.fill( new Ellipse2D.Double( 5, 30, 65, 100 ) ); // dibuja rectángulo en 2D de color rojo g2d.setPaint( Color.RED ); g2d.setStroke( new BasicStroke( 10.0f ) ); g2d.draw( new Rectangle2D.Double( 80, 30, 65, 100 ) ); // dibuja rectángulo delimitador en 2D, con un fondo con búfer BufferedImage imagenBuf = new BufferedImage( 10, 10, BufferedImage.TYPE_INT_RGB ); // obtiene objeto Graphics2D de imagenBuf y dibuja en él Graphics2D gg = imagenBuf.createGraphics(); gg.setColor( Color.YELLOW ); // dibuja en color amarillo gg.fillRect( 0, 0, 10, 10 ); // dibuja un rectángulo relleno gg.setColor( Color.BLACK ); // dibuja en color negro gg.drawRect( 1, 1, 6, 6 ); // dibuja un rectángulo gg.setColor( Color.BLUE ); // dibuja en color azul gg.fillRect( 1, 1, 3, 3 ); // dibuja un rectángulo relleno gg.setColor( Color.RED ); // dibuja en color rojo gg.fillRect( 4, 4, 3, 3 ); // dibuja un rectángulo relleno // pinta a imagenBuf en el objeto JFrame g2d.setPaint( new TexturePaint( imagenBuf, new Rectangle( 10, 10 ) ) ); g2d.fill( new RoundRectangle2D.Double( 155, 30, 75, 100, 50, 50 ) ); // dibuja arco en forma de pastel en 2D, de color blanco g2d.setPaint( Color.WHITE );

Figura 12.29 | Figuras de Java 2D. (Parte 1 de 2).

www.elsolucionario.net

12.8

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75

La API Java 2D

565

g2d.setStroke( new BasicStroke( 6.0f ) ); g2d.draw( new Arc2D.Double( 240, 30, 75, 100, 0, 270, Arc2D.PIE ) ); // dibuja líneas 2D en verde y amarillo g2d.setPaint( Color.GREEN ); g2d.draw( new Line2D.Double( 395, 30, 320, 150 ) ); // dibuja línea 2D usando el trazo float guiones[] = { 10 }; // especifica el patrón de guiones g2d.setPaint( Color.YELLOW ); g2d.setStroke( new BasicStroke( 4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 10, guiones, 0 ) ); g2d.draw( new Line2D.Double( 320, 30, 395, 150 ) ); } // fin del método paintComponent } // fin de la clase FigurasJPanel

Figura 12.29 | Figuras de Java 2D. (Parte 2 de 2).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

// Fig. 12.30: Figuras.java // Demostración de algunas figuras de Java 2D. import javax.swing.JFrame; public class Figuras { // ejecuta la aplicación public static void main( String args[] ) { // crea marco para objeto FigurasJPanel JFrame marco = new JFrame( "Dibujo de figuras en 2D" ); marco.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); // crea objeto FigurasJPanel FigurasJPanel figurasJPanel = new FigurasJPanel(); marco.add( figurasJPanel ); // agrega figurasJPanel to marco marco.setSize( 425, 200 ); // establece el tamaño del marco marco.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase Figuras

Figura 12.30 | Creación de un objeto JFrame para mostrar figuras.

En la línea 25 de la figura 12.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 Java 2D.

www.elsolucionario.net

566

Capítulo 12

Gráficos y Java 2D™

Óvalos, rellenos con degradado y objetos Paint La 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 previamente declarados, los cuales se presentaron en la sección 12.3 (la clase Color implementa a Paint), o el objeto Paint puede ser una instancia de las clases GradientPaint, SystemColor, TexturePaint, LinearGradientPain o RadientGradientPaint de la API Java2D. 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 argumentos. Los primeros dos especifican la coordenada inicial del degradado. El tercer argumento especifica el Color inicial del degradado. Los argumentos cuarto y quinto especifican la coordenada final del degradado. El sexto especifica el Color final del degradado y el último especifica si el degradado es cíclico (true) o acíclico (false). Los dos conjuntos de coordenadas determinan la direcció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 especificado (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 Graphics2D 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 cuatro argumentos que especifican las coordenadas x y y de la esquina superior izquierda, la anchura y la altura del rectángulo.

Rectángulos redondeados, objetos BufferedImage y TexturePaint A continuación dibujamos un rectángulo redondeado, relleno con un patrón creado en un objeto BufferedImage (paquete java.awt.image). En las líneas 38 y 39 se crea el objeto BufferedImage. 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 especificado 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 (descritos anteriormente en este capítulo) 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 asociado (el primer argumento del constructor) como la textura para rellenar una figura. El segundo argumento 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.

www.elsolucionario.net

12.8

La API Java 2D

567

En las líneas 55 y 56 se utiliza el método fill de Graphics2D para dibujar un objeto Shape relleno; 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.

Arcos A 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étodo 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 argumento 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 constantes 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íneas Finalmente, 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 constructor 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 debe utilizarse para describir 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 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 generales A continuación presentaremos una ruta general: una figura compuesta de líneas rectas y curvas complejas. Una ruta general se representa con un objeto de la clase GeneralPath (paquete java.awt.geom). La aplicación de las figuras 12.31 y 12.32 demuestra cómo dibujar una ruta general, en forma de una estrella de cinco puntas.

1 2 3 4 5 6 7 8 9 10 11 12 13

// Fig. 12.31: Figuras2JPanel.java // Demostración de una ruta general. import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.geom.GeneralPath; import java.util.Random; import javax.swing.JPanel; public class Figuras2JPanel extends JPanel { // dibuja rutas generales public void paintComponent( Graphics g )

Figura 12.31 | Rutas generales de Java 2D. (Parte 1 de 2).

www.elsolucionario.net

568

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

Capítulo 12

Gráficos y Java 2D™

{ super.paintComponent( g ); // llama al método paintComponent de la superclase Random aleatorio = new Random(); // obtiene el generador de números aleatorios int puntosX[] = { 55, 67, 109, 73, 83, 55, 27, 37, 1, 43 }; int puntosY[] = { 0, 36, 36, 54, 96, 72, 96, 54, 36, 36 }; Graphics2D g2d = ( Graphics2D ) g; GeneralPath estrella = new GeneralPath(); // crea objeto GeneralPath // establece la coordenada inicial de la ruta general estrella.moveTo( puntosX[ 0 ], puntosY[ 0 ] ); // crea la estrella; esto no la dibuja for ( int cuenta = 1; cuenta < puntosX.length; cuenta++ ) estrella.lineTo( puntosX[ cuenta ], puntosY[ cuenta ] ); estrella.closePath(); // cierra la figura g2d.translate( 200, 200 ); // traslada el origen a (200, 200) // gira alrededor del origen y dibuja estrellas en colores aleatorios for ( int cuenta = 1; cuenta = 0 assert ( numero >= 0 && numero 0), primer nombre, apellido paterno y saldo.", "? " ); while ( entrada.hasNext() ) // itera hasta encontrar el indicador de fin de archivo { try // envía valores al archivo { // obtiene los datos que se van a enviar registro.establecerCuenta( entrada.nextInt() ); // lee el número de cuenta

Figura 14.7 | Creación de un archivo de texto secuencial. (Parte 1 de 2).

www.elsolucionario.net

14.5

60 61

621

registro.establecerPrimerNombre( entrada.next() ); // lee el primer nombre registro.establecerApellidoPaterno( entrada.next() ); // lee el apellido paterno registro.establecerSaldo( entrada.nextDouble() ); // lee el saldo

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99

Archivos de texto de acceso secuencial

if ( registro.obtenerCuenta() > 0 ) { // escribe el nuevo registro salida.format( "%d %s %s %.2f\n", registro.obtenerCuenta(), registro.obtenerPrimerNombre(), registro.obtenerApellidoPaterno(), registro.obtenerSaldo() ); } // fin de if else { System.out.println( "El numero de cuenta debe ser mayor que 0." ); } // fin de else } // fin de try catch ( FormatterClosedException formatterClosedException ) { System.err.println( "Error al escribir en el archivo." ); return; } // fin de catch catch ( NoSuchElementException elementException ) { System.err.println( "Entrada invalida. Intente de nuevo." ); entrada.nextLine(); // descarta la entrada para que el usuario intente de nuevo } // fin de catch System.out.printf( "%s %s\n%s", "Escriba el numero de cuenta (> 0),", "primer nombre, apellido paterno y saldo.", "? " ); } // fin de while } // fin del método agregarRegistros // cierra el file public void cerrarArchivo() { if ( salida != null ) salida.close(); } // fin del método cerrarArchivo } // fin de la clase CrearArchivoTexto

Figura 14.7 | Creación de un archivo de texto secuencial. (Parte 2 de 2).

Sistema operativo

Combinación de teclas

UNIX/Linux/Mac OS X

d

Windows

z

Figura 14.8 | Combinaciones de teclas de fin de archivo para diversos sistemas operativos. En las líneas 59 a 62 se leen datos del usuario y se almacena la información del registro en el objeto RegisCada 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, una cadena cuando se espera un 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 troCuenta.

www.elsolucionario.net

622

Capítulo 14 Archivos y flujos

se escribe en clientes.txt (líneas 67 a 69) mediante el método format. Este método 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. Este método envía una cadena 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 una cadena (el primer nombre), otra cadena (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. Los datos en el archivo de texto se pueden ver con un editor, o posteriormente mediante un programa diseñado para leer el archivo (14.5.2) y obtener esos datos. Cuando se ejecutan las líneas 67 a 69, si se cierra el objeto Formatter se lanza una excepción tipo FormatterClosedException (que 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 el método format para enviar/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. La figura 14.9 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 14.10. En la ejecución de ejemplo para este programa, 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 aparecen 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 abriendo el archivo en un editor de texto.

1 2 3 4 5 6 7 8 9 10 11 12 13 14

// Fig. 14.9: PruebaCrearArchivoTexto.java // Prueba de la clase CrearArchivoTexto. public class PruebaCrearArchivoTexto { public static void main( String args[] ) { CrearArchivoTexto aplicacion = new CrearArchivoTexto(); aplicacion.abrirArchivo(); aplicacion.agregarRegistros(); aplicacion.cerrarArchivo(); } // fin de main } // 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 d y oprima Intro En Windows escriba z y oprima Intro Escriba el numero de cuenta ? 100 Bob Jones 24.98 Escriba el numero de cuenta ? 200 Steve Doe -345.67 Escriba el numero de cuenta ? 300 Pam White 0.00 Escriba el numero de cuenta ? 400 Sam Stone -42.16

(> 0), primer nombre, apellido paterno y saldo. (> 0), primer nombre, apellido paterno y saldo. (> 0), primer nombre, apellido paterno y saldo. (> 0), primer nombre, apellido paterno y saldo.

Figura 14.9 | Prueba de la clase CrearArchivoTexto. (Parte 1 de 2).

www.elsolucionario.net

14.5

Archivos de texto de acceso secuencial

623

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

Figura 14.9 | Prueba de la clase CrearArchivoTexto. (Parte 2 de 2).

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

Figura 14.10 | Datos de ejemplo para el programa de la figura 14.7.

14.5.2 Cómo leer datos de un archivo de texto de acceso secuencial Los datos se almacenan en archivos, para poder procesarlos según sea necesario. En la sección 14.5.1 demostramos cómo crear un archivo para acceso secuencial. Esta sección muestra cómo leer los datos secuencialmente desde un archivo de texto. En esta sección, demostraremos cómo puede utilizarse la clase Scanner para recibir datos de un archivo, en vez del teclado. La aplicación de las figuras 14.11 y 14.12 lee registros del archivo "clientes.txt" creado por la aplicación de la sección 14.5.1 y muestra el contenido de los registros. En la línea 13 de la figura 14.11 se declara un objeto Scanner, que se utilizará para obtener los datos de entrada del archivo. El método abrirArchivo (líneas 16 a 27) abre el archivo en modo de lectura, creando una instancia 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 ejecuta 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.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

// Fig. 14.11: LeerArchivoTexto.java // Este programa lee un archivo de texto y muestra cada registro. import java.io.File; import java.io.FileNotFoundException; import java.lang.IllegalStateException; import java.util.NoSuchElementException; import java.util.Scanner; import com.deitel.jhtp7.cap14.RegistroCuenta; public class LeerArchivoTexto { private Scanner entrada; // permite al usuario abrir el archivo public void abrirArchivo() { try

Figura 14.11 | Lectura de un archivo secuencial mediante un objeto Scanner. (Parte 1 de 2).

www.elsolucionario.net

624

19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72

Capítulo 14 Archivos y flujos

{ entrada = new Scanner( new File( "clientes.txt" ) ); } // fin de try catch ( FileNotFoundException fileNotFoundException ) { System.err.println( "Error al abrir el archivo." ); System.exit( 1 ); } // fin de catch } // fin del método abrirArchivo // lee registro del archivo public void leerRegistros() { // objeto que se va a escribir en la pantalla RegistroCuenta registro = new RegistroCuenta(); System.out.printf("%-9s%-15s%-18s%10s\n”, “Cuenta”, "Primer nombre", "Apellido paterno", "Saldo" ); try // lee registros del archivo, usando el objeto Scanner { while ( entrada.hasNext() ) { registro.establecerCuenta( entrada.nextInt() ); // lee el número de cuenta registro.establecerPrimerNombre( entrada.next() ); // lee el primer nombre registro.establecerApellidoPaterno( entrada.next() ); // lee el apellido paterno registro.establecerSaldo( entrada.nextDouble() ); // lee el saldo // muestra el contenido del registro System.out.printf( " 0), primer nombre, apellido y saldo.", "? " ); while ( entrada.hasNext() ) // itera hasta el indicador de fin de archivo { try // envía los valores al archivo { numeroCuenta = entrada.nextInt(); // lee el número de cuenta primerNombre = entrada.next(); // lee el primer nombre apellidoPaterno = entrada.next(); // lee el apellido paterno saldo = entrada.nextDouble(); // lee el saldo if ( numeroCuenta > 0 ) { // crea un registro nuevo registro = new RegistroCuentaSerializable( numeroCuenta, primerNombre, apellidoPaterno, saldo ); salida.writeObject( registro ); // envía el registro como salida } // fin de if else { System.out.println( "El numero de cuenta debe ser mayor de 0." ); } // fin de else } // fin de try catch ( IOException ioException ) { System.err.println( "Error al escribir en el archivo." ); return; } // fin de catch catch ( NoSuchElementException elementException ) { System.err.println( "Entrada invalida. Intente de nuevo." ); entrada.nextLine(); // descarta la entrada para que el usuario intente de nuevo } // fin de catch

Figura 14.18 | Archivo secuencial creado mediante ObjectOutputStream. (Parte 2 de 3).

www.elsolucionario.net

14.6

82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102

Serialización de objetos

System.out.printf( "%s %s\n%s", "Escriba el numero de cuenta (>0),", "primer nombre, apellido y saldo.", "? " ); } // fin de while } // fin del método agregarRegistros // cierra el archivo y termina la aplicación public void cerrarArchivo() { try // cierra el archivo { if ( salida != null ) salida.close(); } // fin de try catch ( IOException ioException ) { System.err.println( "Error al cerrar el archivo." ); System.exit( 1 ); } // fin de catch } // fin del método cerrarArchivo } // fin de la clase CrearArchivoSecuencial

Figura 14.18 | Archivo secuencial creado mediante ObjectOutputStream. (Parte 3 de 3).

1 2 3 4 5 6 7 8 9 10 11 12 13 14

// Fig. 14.19: PruebaCrearArchivoSecuencial.java // Prueba de la clase CrearArchivoSecuencial. public class PruebaCrearArchivoSecuencial { public static void main( String args[] ) { CrearArchivoSecuencial aplicacion = new CrearArchivoSecuencial(); aplicacion.abrirArchivo(); aplicacion.agregarRegistros(); aplicacion.cerrarArchivo(); } // fin de main } // 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 d y oprima Intro En Windows escriba z y oprima Intro Escriba el numero de cuenta ? 100 Bob Jones 24.98 Escriba el numero de cuenta ? 200 Steve Doe -345.67 Escriba el numero de cuenta ? 300 Pam White 0.00 Escriba el numero de cuenta ? 400 Sam Stone -42.16 Escriba el numero de cuenta ? 500 Sue Rich 224.62 Escriba el numero de cuenta ? ^Z

(> 0), primer nombre, apellido y saldo. (> 0), primer nombre, apellido y saldo. (> 0), primer nombre, apellido y saldo. (> 0), primer nombre, apellido y saldo. (> 0), primer nombre, apellido y saldo. (> 0), primer nombre, apellido y saldo.

Figura 14.19 | Prueba de la clase CrearArchivoSecuencial.

www.elsolucionario.net

635

636

Capítulo 14 Archivos y flujos

Error común de programación 14.2 Es un error lógico abrir un archivo existente en modo de salida cuando, de hecho, el usuario desea preservar ese archivo.

La clase FileOutputStream cuenta con métodos para escribir arreglos tipo byte y objetos byte individuales en un archivo. En este programa deseamos escribir objetos en un archivo; una capacidad que no proporciona FileOutputStream. Por esta razón, envolvemos un objeto FileOutputStream en un objeto ObjectOutputStream, pasando el nuevo objeto FileOutputStream 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 archivo 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) cierra el archivo. Este método llama al método close de ObjectOutputStream en salida para cerrar el objeto ObjectOutputStream y su objeto FileOutputStream subyacente (línea 94). Observe que la llamada al método close está dentro de un bloque try. El método close lanza una excepción IOException 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 exterior también se cierra el archivo subyacente. En la ejecución de ejemplo para el programa de la figura 14.19, introdujimos información para cinco cuentas; la misma información que se muestra en la figura 14.10. El programa no muestra cómo aparecen realmente los registros en el archivo. Recuerde que ahora estamos usando archivos binarios, que no pueden ser leídos por los humanos. Para verificar que el archivo se haya creado exitosamente, la siguiente sección presenta un programa para leer el contenido del archivo.

14.6.2 Lectura y deserialización de datos de un archivo de acceso secuencial Como vimos en la sección 14.5.2, los datos se almacenan en archivos, para que puedan obtenerse y procesarse según sea necesario. En 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 14.20 y 14.21 lee registros de un archivo creado por el programa de la sección 14.6.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 14.18 escribimos objetos al archivo, usando un objeto ObjectOutputStream. Los datos se deben leer del archivo en el mismo formato en el que se escribió. 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. 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 descendente 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 14.21 contiene el método main (líneas 6 a 13), el cual abre el archivo, llama al método leerRegistros y cierra el archivo.

www.elsolucionario.net

14.6

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

Serialización de objetos

// Fig. 14.20: LeerArchivoSecuencial.java // Este programa lee un archivo de objetos en forma secuencial // y muestra cada registro. import java.io.EOFException; import java.io.FileInputStream; import java.io.IOException; import java.io.ObjectInputStream; import com.deitel.jhtp7.cap14.RegistroCuentaSerializable; public class LeerArchivoSecuencial { private ObjectInputStream entrada; // permite al usuario seleccionar el archivo a abrir public void abrirArchivo() { try // abre el archivo { entrada = new ObjectInputStream( new FileInputStream( "clientes.ser" ) ); } // fin de try catch ( IOException ioException ) { System.err.println( "Error al abrir el archivo." ); } // fin de catch } // fin del método abrirArchivo // lee el registro del archivo public void leerRegistros() { RegistroCuentaSerializable registro; System.out.printf( "%-10s%-15s%-15s%10s\n", "Cuenta", "Primer nombre", "Apellido paterno", "Saldo" ); try // recibe los valores del archivo { while ( true ) { registro = ( RegistroCuentaSerializable ) entrada.readObject(); // muestra el contenido del registro System.out.printf( "%-10d%-15s%-15s%11.2f\n", registro.obtenerCuenta(), registro.obtenerPrimerNombre(), registro.obtenerApellidoPaterno(), registro.obtenerSaldo() ); } // fin de while } // fin de try catch ( EOFException endOfFileException ) { return; // se llegó al fin del archivo } // fin de catch catch ( ClassNotFoundException classNotFoundException ) { System.err.println( "No se pudo crear el objeto." ); } // fin de catch catch ( IOException ioException ) { System.err.println( "Error al leer el archivo." ); } // fin de catch

Figura 14.20 | Lectura de un archivo secuencial, usando un objeto ObjectInputStream. (Parte 1 de 2).

www.elsolucionario.net

637

638

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77

Capítulo 14 Archivos y flujos

} // fin del método leerRegistros // cierra el archivo y termina la aplicación public void cerrarArchivo() { try // cierra el archivo y sale { if ( entrada != null ) entrada.close(); System.exit( 0 ); } // fin de try catch ( IOException ioException ) { System.err.println( "Error al cerrar el archivo." ); System.exit( 1 ); } // fin de catch } // fin del método cerrarArchivo } // fin de la clase LeerArchivoSecuencial

Figura 14.20 | Lectura de un archivo secuencial, usando un objeto ObjectInputStream. (Parte 2 de 2).

1 2 3 4 5 6 7 8 9 10 11 12 13 14

// Fig. 14.21: PruebaLeerArchivoSecuencial.java // Este programa prueba la clase ReadSequentialFile. public class PruebaLeerArchivoSecuencial { public static void main( String args[] ) { LeerArchivoSecuencial aplicacion = new LeerArchivoSecuencial(); aplicacion.abrirArchivo(); aplicacion.leerRegistros(); aplicacion.cerrarArchivo(); } // fin de main } // fin de la clase PruebaLeerArchivoSecuencial

Cuenta 100 200 300 400 500

Primer nombre Bob Steve Pam Sam Sue

Apellido paterno Jones Doe White Stone Rich

Saldo 24.98 -345.67 0.00 -42.16 224.62

Figura 14.21 | Prueba de la clase LeerArchivoSecuencial.

14.7 Clases adicionales de java.io

Ahora le presentaremos otras clases útiles en el paquete java.io. Veremos las generalidades acerca de las interfaces y clases adicionales para los flujos de entrada y salida basados en bytes, y los flujos de entrada y salida basados en caracteres.

Interfaces y clases para la entrada y salida basadas en bytes y OutputStream (subclases de Object) son clases abstract que declaran métodos para realizar operaciones basadas en bytes de entrada y salida, respectivamente. En este capítulo utilizamos las clases concretas FileInputStream (una subclase de InputStream) y FileOutputStream (una subclase de OutputStream) para manipular archivos. InputStream

www.elsolucionario.net

14.7

Clases adicionales de java.io

639

Las canalizaciones son canales de comunicación sincronizados entre subprocesos; hablaremos sobre los subprocesos en el capítulo 23, Subprocesamiento múltiple. Java proporciona las clases PipedOutputStream (una subclase de OutputStream) y PipedInputStream (una subclase de InputStream) para establecer canalizaciones entre dos subprocesos en un programa. Un subproceso envía datos a otro, escribiendo a un objeto PipedOutputStream. El subproceso de destino lee la información de la canalización mediante un objeto PipedInputStream. Un 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 proporciona una funcionalidad adicional, como la agregación de bytes de datos en unidades de tipo primitivo significativas. FilterInputStream y FilterOutputStream son clases abstract, por lo que sus subclases concretas 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. Leer 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 un valor int, un float, un double y así, sucesivamente. 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 los métodos readLine (para arreglos byte), readBoolean, readByte, readChar, readDouble, readFloat, readFully (para arreglos byte), readInt, readLong, readShort, readUnsignedByte, readUnsignedShort, readUTF (para leer caracteres Unicode codificados por Java; hablaremos sobre la codificación UTF en el apéndice I, Unicode®) 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 RandomAccessFile implementan a esta interfaz para escribir valores de tipos primitivos como bytes. La interfaz DataOutput incluye versiones sobrecargadas del método write (para un byte o para arreglo byte) y los métodos writeBoolean, writeByte, writeBytes, writeChar, writeChars (para objetos String Unicode), writeDouble, writeFloat, writeInt, writeLong, writeShort y writeUTF (para enviar texto modificado para Unicode). El 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 suficientemente grande 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 operaciones lógicas de salida. Con un objeto BufferedOutputStream, se puede forzar a un búfer parcialmente 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 considerablemente la eficiencia de una aplicación. Las operaciones comunes de E/S son extremadamente lentas, en comparación con la velocidad de acceso 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 14.1 La 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 trozos “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 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 dispositivo de entrada, para leer el siguiente grupo de trozos “lógicos” de

www.elsolucionario.net

640

Capítulo 14 Archivos y flujos

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 lectura emitidas por el programa. La 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 subclase 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. Finalmente, 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. Un objeto SequenceInputStream (una subclase de InputStream) permite la concatenación de varios objetos InputStream, por lo que el programa ve al grupo como un flujo InputStream continuo. Cuando el programa llega al final de un flujo de entrada, ese flujo se cierra y se abre el siguiente flujo en la secuencia.

Interfaces y clases para la entrada y salida basadas en caracteres Además de los flujos basados en caracteres, Java proporciona las clases abstractas Reader y Writer, que son flujos basados en caracteres Unicode de dos bytes. La mayoría de los flujos basados en caracteres tienen sus correspondientes clases Reader o Writer basadas en caracteres. Las clases BufferedReader (una subclase de la clase abstract Reader) y BufferedWriter (una subclase de la clase abstract Writer) permiten el uso del búfer para los flujos basados en caracteres. Recuerde 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. Las clases CharArrayReader y CharArrayWriter leen y escriben, respectivamente, un flujo de caracteres en un arreglo de caracteres. 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 (es decir, una nueva línea, un retorno o una combinación de retorno de carro y avance de línea). 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. 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 la información entre subprocesos. Las clases StringReader y StringWriter leen y escriben caracteres, respectivamente, en objetos String. Un objeto PrintWriter escribe caracteres en un flujo.

14.8 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 14.4, como se muestra en las figuras 14.22 y 14.23. 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. El método getFile se define en las líneas 38 a 62 de la figura 14.22. 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 selectorArchivos. 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 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álogo JFileChooser, la

www.elsolucionario.net

14.8 Abrir archivos con

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57

JFileChooser

641

// Fig. 14.22: DemostracionFile.java // Demostración de la clase File. import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JTextField; public class DemostracionFile extends JFrame { private JTextArea areaSalida; // se utiliza para salida private JScrollPane panelDespl; // se utiliza para que la salida pueda desplazarse // establece la GUI public DemostracionFile() { super( "Prueba de la clase File" ); areaSalida = new JTextArea(); // agrega areaSalida a panelDespl panelDespl = new JScrollPane( areaSalida ); add( panelDespl, BorderLayout.CENTER ); // agrega panelDespl a la GUI setSize( 400, 400 ); // establece el tamaño de la GUI setVisible( true ); // muestra la GUI analizarRuta(); // crea y analiza un objeto File } // fin del constructor de DemostracionFile // permite al usuario especificar el nombre del archivo private File obtenerArchivo() { // muestra el cuadro de diálogo de archivos, para que el usuario pueda elegir el archivo a abrir JFileChooser selectorArchivos = new JFileChooser(); selectorArchivos.setFileSelectionMode( JFileChooser.FILES_AND_DIRECTORIES ); int resultado = selectorArchivos.showOpenDialog( this ); // si el usuario hizo clic en el botón Cancelar en el cuadro de diálogo, regresa if ( resultado == JFileChooser.CANCEL_OPTION ) System.exit( 1 ); File nombreArchivo = selectorArchivos.getSelectedFile(); // obtiene el archivo seleccionado // muestra error si es inválido if ( ( nombreArchivo == null ) || ( nombreArchivo.getName().equals( "" ) ) ) { JOptionPane.showMessageDialog( this, "Nombre de archivo inválido", "Nombre de archivo inválido", JOptionPane.ERROR_MESSAGE );

Figura 14.22 | Demostración de JFileChooser. (Parte 1 de 2).

www.elsolucionario.net

642

58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100

Capítulo 14 Archivos y flujos

System.exit( 1 ); } // fin de if return nombreArchivo; } // fin del método obtenerArchivo // muestra información acerca del archivo que especifica el usuario public void analizarRuta() { // crea un objeto File basado en la entrada del usuario File nombre = obtenerArchivo(); if ( nombre.exists() ) // si el nombre existe, muestra información sobre él { // muestra la información sobre el archivo (o directorio) areaSalida.setText( String.format( "%s%s\n%s\n%s\n%s\n%s%s\n%s%s\n%s%s\n%s%s\n%s%s", nombre.getName(), " existe", ( nombre.isFile() ? "es un archivo" : "no es un archivo" ), ( nombre.isDirectory() ? "es un directorio" : "no es un directorio" ), ( nombre.isAbsolute() ? "es una ruta absoluta" : "no es una ruta absoluta" ), "Ultima modificacion: ", nombre.lastModified(), "Tamanio: ", nombre.length(), "Ruta: ", nombre.getPath(), "Ruta absoluta: ", nombre.getAbsolutePath(), "Padre: ", nombre.getParent() ) ); if ( nombre.isDirectory() ) // imprime el listado del directorio { String directorio[] = nombre.list(); areaSalida.append( "\n\nContenido del directorio:\n" ); for ( String nombreDirectorio : directorio ) areaSalida.append( nombreDirectorio + "\n" ); } // fin de else } // fin de if exterior else // no es archivo ni directorio, imprime mensaje de error { JOptionPane.showMessageDialog( this, nombre + " no existe.", "ERROR", JOptionPane.ERROR_MESSAGE ); } // fin de else } // fin del método analizarRuta } // fin de la clase DemostracionFile

Figura 14.22 | Demostración de JFileChooser. (Parte 2 de 2). 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, llamando al método getSelectedFile de selectorArchivos. Después, el programa muestra información acerca del archivo o directorio seleccionado.

www.elsolucionario.net

14.9

1 2 3 4 5 6 7 8 9 10 11 12

Conclusión

643

// Fig. 14.23: PruebaDemostracionFile.java // Prueba de la clase DemostracionFile. import javax.swing.JFrame; public class PruebaDemostracionFile { public static void main( String args[] ) { DemostracionFile aplicacion = new DemostracionFile(); aplicacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); } // fin de main } // fin de la clase PruebaDemostracionFile

Seleccione aquí la ubicación del archivo o directorio Haga clic en Abrir para enviar el nombre del archivo o directorio al programa

Aquí se muestran los archivos y directorios

Figura 14.23 | Prueba de la clase DemostracionFile.

14.9 Conclusión En este capítulo aprendió a utilizar el procesamiento de archivos para manipular datos persistentes. Aprendió que los datos se almacenan en las computadoras en forma de 0s y 1s, y que las combinaciones de estos valores se

www.elsolucionario.net

644

Capítulo 14 Archivos y flujos

utilizan para formar bytes, campos, registros y, en un momento dado, archivos. 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 una descripción general de las demás clases que proporciona el paquete java.io, y 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. Al definir métodos de esta forma, podemos producir programas más intuitivos.

Resumen Sección 14.1 Introducción • Los datos que se almacenan en variables y arreglos son temporales; 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 grandes cantidades de datos, incluso después de que los programas que crearon los datos terminan de ejecutarse. • Los datos persistentes que se mantienen en archivos existen más allá de la duración de la ejecución del programa. • Las computadoras almacenan los archivos en dispositivos de almacenamiento secundario, como los discos duros.

Sección 14.2 Jerarquía de datos • El elemento de datos más pequeño en una computadora puede asumir el valor 0 o 1, y se le conoce como bit. En última instancia, una computadora procesa todos los elementos de datos como combinaciones de ceros y unos. • El conjunto de caracteres de la computadora es el conjunto de todos los caracteres que se utilizan para escribir programas y representar datos. • Los caracteres en Java son Unicode y están compuestos de dos bytes, cada uno de los cuales se compone de ocho bits. • Así como los caracteres están compuestos de bits, los campos se componen de caracteres o bytes. Un campo es un grupo de caracteres o bytes que transmite un significado. • Los elementos de datos procesados por las computadoras forman una jerarquía de datos, la cual se vuelve más grande y compleja en estructura, a medida que progresamos de bits a caracteres, luego a campos, y así en lo sucesivo. • Por lo general, varios campos componen un registro (que se implementa como class en Java). • Un registro es un grupo de campos relacionados. • Un archivo es un grupo de registros relacionados. • Para facilitar la obtención de registros específicos de un archivo, se elije por lo menos un campo en cada registro como clave. Una clave de registro identifica que un registro pertenece a una persona o entidad específica, y es único para cada registro. • Existen muchas formas de organizar los registros en un archivo. La más común se llama archivo secuencial, en el cual los registros se almacenan en orden, en base al campo clave de registro. • Por lo general, a un grupo de archivos relacionados se le denomina base de datos. Una colección de programas diseñados para crear y administrar bases de datos se conoce como sistema de administración de bases de datos (DBMS).

Sección 14.3 Archivos y flujos • Java ve a cada archivo como un flujo secuencial de bytes. • Cada sistema operativo cuenta con un mecanismo para determinar el fin de un archivo, como un marcador de fin de archivo o la cuenta de los bytes totales en el archivo, que se registra en una estructura de datos administrativa, manejada por el sistema. • Los flujos basados en bytes representan datos en formato binario. • Los flujos basados en caracteres representan datos como secuencias de caracteres. • Los archivos que se crean usando flujos basados en bytes son archivos binarios. Los archivos que se crean usando flujos basados en caracteres son archivos de texto. Los archivos de texto se pueden leer mediante editores de texto,

www.elsolucionario.net

Resumen

645

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 flujos con distintos dispositivos. Tres objetos flujo se asocian con dispositivos cuando un programa de Java empieza a ejecutarse: System.in, System.out y System.err. • El paquete java.io incluye definiciones para las clases de flujos, como FileInputStream (para la entrada basada en bytes de un archivo), FileOutputStream (para la salida basada en bytes hacia un archivo), FileReader (para la entrada basada en caracteres de un archivo) y FileWriter (para la salida basada en caracteres hacia un archivo). Los archivos se abren creando objetos de estas clases de flujos.

Sección 14.4 La clase File • La clase File 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 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 especifica su ubicación en el disco. • Una ruta absoluta contiene todos los directorios, empezando con el directorio raíz, que conducen hacia un archivo o directorio específico. Cada archivo o directorio en una unidad de disco tiene el mismo directorio raíz en su ruta. • Por lo general, una ruta relativa empieza desde el directorio en el que se empezó a ejecutar la aplicación. • Un carácter separador se utiliza para separar directorios y archivos en la ruta.

Sección 14.5 Archivos de texto de acceso secuencial • Java no impone una estructura en un archivo; las nociones como los registros no existen como parte del lenguaje de Java. 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 a leer 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 modificar sin el riesgo de destruir otros datos en el archivo. Por lo tanto, los registros en un archivo de acceso secuencial normalmente no se actualizan directamente en su ubicación. En vez de ello, se vuelve a escribir el archivo completo.

Sección 14.6 Serialización de objetos • • • •



Java cuenta con un mecanismo llamado serialización de objetos, el cual permite escribir o leer objetos completos mediante un flujo. Un objeto serializado es un objeto que 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 almacenados en el mismo. Una vez que se escribe un objeto serializado en un archivo, se puede leer del archivo y deserializarse; es decir, se puede utilizar la información de tipo y los bytes que representan al objeto para recrearlo en la memoria. Las clases ObjectInputStream y ObjectOutputStream, que implementan en forma respectiva a las interfaces ObjectInput y ObjectOutput, permiten leer o escribir objetos completos de/a un flujo (posiblemente un archivo). Sólo las clases que implementan a la interfaz Serializable pueden serializarse y deserializarse con objetos ObjectOutputStream y ObjectInputStream.

Sección 14.7 Clases adicionales de java.io • La interfaz ObjectOutput contiene el método writeObject, el cual recibe un objeto Object que implementa a la interfaz Serializable como argumento y escribe su información en un objeto OutputStream. • La interfaz ObjectInput contiene el método readObject, que lee y devuelve una referencia a un objeto Object de un objeto InputStream. Una vez que se ha leído un objeto, su referencia puede convertirse al tipo actual del objeto. • El uso de búfer es una técnica para mejorar el rendimiento de E/S. Con un objeto BufferedOutputStream, cada instrucción de salida no necesariamente produce una transferencia física real de datos al dispositivo de salida. En vez de ello, cada operación de salida se dirige hacia una región en memoria llamada búfer, la cual es lo bastante grande como para contener los datos de muchas operaciones de salida. La transferencia actual al dispositivo de salida se realiza entonces en una sola operación de salida física extensa cada vez que se llena el búfer. • Con un objeto BufferedInputStream, muchos trozos “lógicos” de datos de un archivo se leen como una sola operación de entrada física extensa y se colocan en un búfer de memoria. A medida que un programa solicita cada nuevo trozo de datos, se obtiene del búfer. Cuando el búfer está vacío, se lleva a cabo la siguiente operación de entrada física real desde el dispositivo de entrada, para leer el nuevo grupo de trozos “lógicos” de datos.

www.elsolucionario.net

646

Capítulo 14 Archivos y flujos

Sección 14.8 Abrir archivos con JFileChooser • La clase JFileChooser se utiliza para mostrar un cuadro de diálogo, que permite a los usuarios de un programa seleccionar archivos con facilidad, mediante una GUI.

Terminología aplicación de acceso directo apuntador de posición de archivo archivo archivo binario archivo de acceso secuencial archivo de procesamiento por lotes archivo de sólo lectura archivo de texto archivos de acceso directo arreglo de bytes envuelto base de datos bit (dígito binario) búfer búfer de memoria byte, tipo de datos campo CANCEL_OPTION, constante de la clase JFileChooser canRead, método de la clase File canWrite, método de la clase File capacidad -classpath, argumento de línea de comandos para java -classpath, argumento de línea de comandos para javac

clave de registro conjunto de caracteres conjunto de caracteres ASCII (Código estándar estadounidense para el intercambio de información) DataInput, interfaz DataInputStream, clase DataOutput, interfaz DataOutputStream, clase datos persistentes dígito decimal DIRECTORIES_ONLY, constante de la clase JFileChooser directorio directorio padre directorio raíz disco disco óptico dispositivos de almacenamiento secundario EndOfFileException, excepción envoltura de objetos flujo exists, método de la clase File exit, método de la clase System File, clase FileInputStream, clase FileOutputStream, clase FileReader, clase FILES_AND_DIRECTORIES, constante de la clase JFileChooser

FILES_ONLY, FileWriter,

constante de la clase JFileChooser clase flujo basado en bytes flujo basado en caracteres flujo de bytes Formatter, clase getAbsolutePath, método de la clase File getName, método de la clase File getParent, método de la clase File getPath, método de la clase File getSelectedFile, método de la clase JFileChooser InputStream, clase interfaz de marcado IOException, excepción isAbsolute, método de la clase File isDirectory, método de la clase File isFile, método de la clase File java.io, paquete jerarquía de datos JFileChooser, clase JFileChooser, cuadro de diálogo lastModified, método de la clase File length, método de la clase File list, método de la clase File marcador de fin de archivo nombre de directorio NoSuchElementException, excepción ObjectInputStream, clase ObjectOutputStream, clase objeto deserializado objeto flujo objeto flujo de error estándar objeto serializado operación física de entrada operación física de salida operaciones lógicas de entrada operaciones lógicas de salida OutputStream, clase pathSeparator, campo static de la clase File PrintStream, clase PrintWriter, clase procesamiento de archivos procesamiento de flujos Reader, clase readLine, método de la clase BufferedReader readObject, método de la clase ObjectInputStream readObject, método de la interfaz ObjectInput registro registro de longitud fija ruta absoluta

www.elsolucionario.net

Ejercicios de autoevaluación ruta relativa secuencia de comandos de shell Serializable, interfaz serialización de objetos setErr, método de la clase System setIn, método de la clase System setOut, método de la clase System setSelectionMode de la clase JFileChooser showOpenDialog de la clase JFileChooser sistema de administración de bases de datos (DBMS) System.err (flujo de error estándar) transient, palabra clave truncada Unicode, conjunto de caracteres URI (Identificador uniforme de recursos)

647

writeBoolean, método de la interfaz DataOutput writeByte, método de la interfaz DataOutput writeBytes, método de la interfaz DataOutput writeChar, método de la interfaz DataOutput writeChars, método de la interfaz DataOutput writeDouble, método de la interfaz DataOutput writeFloat, método de la interfaz DataOutput writeInt, método de la interfaz DataOutput writeLong, método de la interfaz DataOutput writeObject, método de la clase ObjectOutputStream writeObject, método de la interfaz ObjectOutput Writer, clase writeShort, método de la interfaz DataOutput writeUTF, método de la interfaz DataOutput

Ejercicios de autoevaluación 14.1

Complete las siguientes oraciones: a) Básicamente, todos los elementos de datos procesados por una computadora se reducen en combinaciones de ______________ y ______________. b) El elemento de datos más pequeño que puede procesar una computadora se conoce como ____________. c) Un ____________ puede verse algunas veces como un grupo de registros relacionados. d) Los dígitos, letras y símbolos especiales se conocen como____________. e) Una base de datos es un grupo de ____________ relacionados. f ) El objeto ____________ normalmente permite a un programa imprimir mensajes de error en la pantalla.

14.2

Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué. a) El programador debe crear explícitamente los objetos flujo System.in, System.out y System.err. b) Al leer datos de un archivo mediante la clase Scanner, si el programador desea leer datos en el archivo varias veces, el archivo debe cerrarse y volver a abrirse para leer desde el principio del archivo. Esto desplaza el apuntador de posición de archivo de vuelta hasta 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 constructor 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.

14.3

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 variable 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 14.6. 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 RegistroTransaccion 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.

www.elsolucionario.net

648

Capítulo 14 Archivos y flujos 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.

14.4

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 ObjectInputStream 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 ObjectInputStream 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 variable 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 14.17. 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 en el archivo "nuevomaest.ser". El registro es un objeto de tipo RegistroCuenta; use la variable Formatter llamada salNuevoMaest.

14.5

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ón 14.1

a) unos, ceros. b) bit. c) archivo. d) caracteres. e) archivos. f ) System.err.

14.2

a) b) c) d) e) f)

Falso. Estos tres flujos se crean para el programador cuando se empieza a ejecutar una aplicación de Java. Verdadero. Verdadero. Falso. Los archivos de texto pueden ser leídos por los humanos. Verdadero. Falso. La clase Formatter contiene el método format, el cual permite imprimir datos con formato en la pantalla, o enviarlos a un archivo.

14.3

a) b) c) d)

Scanner entAntMaest = new Scanner( new File( "antmaest.txt" ) ); Scanner entTransaccion = new Scanner( new File( "trans.txt" ) ); Formatter salNuevoMaest = new Formatter( "nuevomaest.txt" ); RegistroCuenta

cuenta = new RegistroCuenta();

cuenta.establecerCuenta( entAntMaest.nextInt() ); cuenta.establecerPrimerNombre( entAntMaest.next() ); cuenta.establecerApellidoPaterno( entAntMaest.next() ); cuenta.establecerSaldo( entAntMaest.nextDouble() );

e)

RegistroTransaccion transacción = 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() );

www.elsolucionario.net

Ejercicios

14.4

a)

ObjectInputStream entAntMaest = new ObjectInputStream(

b)

ObjectInputStream entTransaccion = new ObjectInputStream(

c)

ObjectOutputStream salNuevMaest = new ObjectOutputStream(

d) e) f)

registroCuenta = ( RegistroCuentaSerializable ) entAntMaest.readObject();

649

new FileInputStream( "antmaest.ser" ) ); new FileInputStream( "trans.ser" ) ); new FileOutputStream( "nuevmaest.ser" ) );

14.5

registroTransaccion = ( RegistroTransaccion ) entTransaccion.readObject(); salNuevMaest.writeObject( nuevoRegistroCuenta );

a) Error: el archivo no se ha abierto antes de tratar de enviar datos al flujo. Corrección: abrir un archivo en modo de salida, creando un nuevo objeto ObjectOutputStream que envuelva 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.

Ejercicios 14.6

Llene los espacios en blanco en cada uno de los siguientes enunciados: a) Las computadoras almacenan grandes cantidades de datos en dispositivos de almacenamiento secundario, como ____________. b) Un ____________ está compuesto de varios campos. c) Para facilitar la recuperación de registros específicos de un archivo, debe seleccionarse un campo en cada registro como ____________. d) Los archivos que se crean usando flujos basados en bytes se conocen como archivos ____________, mientras que los archivos creados usando flujos basados en caracteres se conocen como archivos ___________. e) Los objetos flujo estándar son ____________, ____________ y ____________.

14.7

Determine cuál de los siguientes enunciados es verdadero y cuál es falso. Si es falso, explique por qué. a) Las impresionantes funciones realizadas por las computadoras involucran esencialmente la manipulación de ceros y unos. b) Las personas especifican los programas y elementos de datos como caracteres. Después, las computadoras manipulan y procesan estos caracteres como grupos de ceros y unos. c) Los elementos de datos que se representan en las computadoras forman una jerarquía de datos, en la cual los elementos de datos se hacen más grandes y complejos, a medida que progresamos de campos a caracteres, de caracteres a bits, etcétera. d) Una clave de registro identifica que un registro pertenece a un campo específico. e) Las compañías almacenan toda su información en un solo archivo, para poder facilitar el procesamiento computacional de la información. Cuando un programa crea un archivo, éste es retenido automáticamente por la computadora para cuando se haga referencia a él en un futuro.

14.8 (Asociación de archivos) El ejercicio de autoevaluación 14.3 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 generalmente 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 final 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 final del siguiente periodo de negocios para empezar de nuevo el proceso de actualización.

www.elsolucionario.net

650

Capítulo 14 Archivos y flujos

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 fines de asociar los archivos. Suponga que cada archivo es un archivo de texto secuencial con registros almacenados en orden ascendente, por número de cuenta. a) Defina 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 modificar y obtener estos valores. b) Modifique la clase RegistroCuenta de la figura 14.6 para incluir el método combinar, el cual recibe un objeto 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 figuras 14.24 y 14.25. Ejecute el programa para crear los archivos trans.txt y antmaest.txt, para que los utilice su programa de asociación de archivos. 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 registro "nuevomaest.txt". (Suponga que las compras se indican mediante montos positivos en el archivo de transacciones, y los pagos mediante montos negativos). Cuado haya un registro maestro para una cuenta específica, pero no haya un registro de transacciones correspondiente, simplemente escriba el registro 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 asociado para ese numero de cliente..." (utilice el número de cuenta del registro de transacciones). El archivo de registro debe ser un archivo de texto llamado "registro.txt". 14.9 (Asociación de archivos con varias transacciones) Es posible (y muy común) tener varios registros de transacciones con la misma clave de registro. 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 14.8, 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 14.26. 14.10 (Asociación de archivos con serialización de objetos) Vuelva a crear su solución para el ejercicio 14.9, usando la serialización de objetos. Use las instrucciones del ejercicio 14.4 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 14.6.2 para este fin.

Número de cuenta

Nombre

Saldo

100

Alan Jones

348.17

300

Mary Smith

27.19

500

Sam Sharp

0.00

700

Susy Green

-14.22

Figura 14.24 | Datos de ejemplo para el archivo maestro.

www.elsolucionario.net

Ejercicios

Archivo de transacciones Número de cuenta

Monto de la transacción

100

27.14

300

62.11

400

100.56

900

82.17

651

Figura 14.25 | Datos de ejemplo para el archivo de transacciones.

Número de cuenta

Monto en dólares

300

83.89

700

80.78

700

1.53

Figura 14.26 | Registros de transacciones adicionales.

14.11 (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 14.27). 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 14.27 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”).

Dígito

Letras

2

A B C

3

D E F

4

G H I

5

J K L

6

M N O

7

P R S

8

T U V

9

W X Y

Figura 14.27 | Dígitos y letras de los teclados telefónicos.

Cada número telefónico de siete letras corresponde a muchas palabras de siete letras distintas. Desafortunadamente, 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

www.elsolucionario.net

652

Capítulo 14 Archivos y flujos

“MUEBLES”. El propietario de una tienda de licores estaría, sin duda, feliz de averiguar que el número telefónico 2324327 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. 14.12 (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. Deberá utilizar el método nextInt para introducir un entero del archivo a la vez. El programa deberá seguir leyendo respuestas hasta que llegue al fin del archivo. Los resultados deberán escribirse en el archivo de texto "salida.txt". 14.13 Modifique el ejercicio 11.18 para permitir 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) y Generar figuras (para mostrar un conjunto aleatorio de figuras en la pantalla). 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). Observe que 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 previamente guardado, o haciendo clic en el botón Generar figuras. Cuando el usuario haga clic en este botón, la aplicación deberá generar un número aleatorio de figuras, hasta un total de 15. Una vez que haya figuras en la pantalla, los usuarios podrán guardarlas en un archivo, usando el botón Guardar.

www.elsolucionario.net

15 Recursividad Debemos 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.

OBJETIVOS En este capítulo aprenderá a: Q

Comprender el concepto de recursividad.

Q

Escribir y utilizar métodos recursivos.

Q

Determinar el caso base y el paso de recursividad en un algoritmo recursivo.

Q

Conocer cómo el sistema maneja las llamadas a métodos recursivos.

Q

Conocer las diferencias entre recursividad e iteración, y cuándo es apropiado utilizar cada una.

Q

Conocer las figuras geométricas llamadas fractales, y cómo se dibujan mediante la recursividad.

Q

Conocer el concepto de “vuelta atrás” recursiva (backtracking), y por qué es una técnica efectiva para solucionar problemas.

—Lewis Carroll

La vida sólo puede comprenderse al revés; pero debe vivirse hacia delante. —Soren Kierkegaard

Empujen; sigan avanzando. —Thomas Morton

www.elsolucionario.net

Pla n g e ne r a l

654

Capítulo 15

15.1 15.2 15.3 15.4 15.5 15.6 15.7 15.8 15.9 15.10 15.11

Recursividad

Introducción Conceptos de recursividad Ejemplo de uso de recursividad: factoriales Ejemplo de uso de recursividad: serie de Fibonacci La recursividad y la pila de llamadas a métodos Comparación entre recursividad e iteración Las torres de Hanoi Fractales “Vuelta atrás” recursiva (backtracking) Conclusión Recursos en Internet y Web

Resumen | Terminología | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

15.1 Introducción Los programas que hemos visto hasta ahora están estructurados generalmente como métodos que se llaman entre sí, de una manera disciplinada y jerárquica. Sin embargo, 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 15.1 se sintetizan los ejemplos y ejercicios de recursividad que se incluyen en este libro.

Capítulo

Ejemplos y ejercicios de recursividad en este libro

15

Método factorial (figuras 15.3 y 15.4.) Método Fibonacci (figuras 15.5 y 15.6). Torres de Hanoi (figuras 15.13 y 15.14). Fractales (figuras 15.21 y 15.22). ¿Qué hace este código? (ejercicios 15.7, 15.12 y 15.13). Encuentre el error en el siguiente código (ejercicio 15.8). Elevar un entero a una potencia entera (ejercicio 15.9). Visualización de la recursividad (ejercicio 15.10). Máximo común divisor (ejercicio 15.11). Determinar si una cadena es un palíndromo (ejercicio 15.14). Ocho reinas (ejercicio 15.15). Imprimir un arreglo (ejercicio 15.16). Imprimir un arreglo al revés (ejercicio 15.17). Mínimo valor en un arreglo (ejercicio 15.18). Fractal de estrella (ejercicio 15.19). Recorrido de un laberinto mediante el uso de la “vuelta atrás” recursiva (ejercicio 15.20). Generación de laberintos al azar (ejercicio 15.21). Laberintos de cualquier tamaño (ejercicio 15.22). Tiempo para calcular números de Fibonacci (ejercicio 15.23).

16

Ordenamiento por combinación (figuras 16.10 y 16.11). Búsqueda lineal recursiva (ejercicio 16.8). Búsqueda binaria recursiva (ejercicio 16.9). Quicksort (ejercicio 16.10).

Figura 15.1 | Resumen de los ejemplos y ejercicios de recursividad en este libro. (Parte 1 de 2).

www.elsolucionario.net

15.3

Ejemplo de uso de recursividad: factoriales

Capítulo

Ejemplos y ejercicios de recursividad en este libro

17

Inserción en árbol binario (figura 17.17). Recorrido preorden de un árbol binario (figura 17.17). Recorrido inorden de un árbol binario (figura 17.17). Recorrido postorden de un árbol binario (figura 17.17). Imprimir una lista en forma recursiva y en forma inversa (ejercicio 17.20). Buscar en una lista en forma recursiva (ejercicio 17.21).

655

Figura 15.1 | Resumen de los ejemplos y ejercicios de recursividad en este libro. (Parte 2 de 2).

15.2 Conceptos de recursividad Los 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, el método comúnmente 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 combina 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 momento dado, cada vez que el método se llama 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. En ese punto, el método reconoce el caso base y devuelve un resultado a la copia anterior del método. Después se origina una secuencia 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 archivos en los subdirectorios de ese directorio), necesitamos crear un método que lea primero los archivos 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 contenga subdirectorios. En este punto, se han enlistado todos los archivos en el directorio original y no se necesita más la recursividad.

15.3 Ejemplo de uso de recursividad: factoriales Escribimos 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

www.elsolucionario.net

656

Capítulo 15

Recursividad

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 v 0) puede calcularse de manera iterativa (sin recursividad), usando una instrucción for de la siguiente manera: factorial = 1; for ( int contador = numero; 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 15.2. La figura 15.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 la recursividad. La figura 15.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 15.3 se utiliza la recursividad para calcular e imprimir los factoriales de los enteros del 0 al 10. El método recursivo factorial (líneas 7 a 13) realiza primero una evaluación para determinar 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 ).

valor final = 120 5!

5!

se devuelve 5! = 5 * 24 = 120 5 * 4!

5 * 4!

se devuelve 4! = 4 * 6 = 24 4 * 3!

4 * 3!

se devuelve 3! = 3 * 2 = 6 3 * 2!

3 * 2!

se devuelve 2! = 2 * 1 = 2 2 * 1!

2 * 1!

se devuelve 1 1

1

(a) Secuencia de llamadas recursivas.

(b) Valores devueltos de cada llamada recursiva.

Figura 15.2 | Evaluación recursiva de 5!.

www.elsolucionario.net

15.3

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

21 22

Ejemplo de uso de recursividad: factoriales

657

// Fig. 15.3: CalculoFactorial.java // Método factorial recursivo. public class CalculoFactorial { // declaración recursiva del método factorial public long factorial( long numero ) { if ( numero --> --> --> --> --> -->

3 2 2 3 1 3 3

Figura 15.14 | Prueba de la solución de las torres de Hanoi.

15.8 Fractales Un fractal es una figura geométrica que se puede generar a partir de un patrón que se repite en forma recursiva (figura 15.15). Para modificar la figura, se aplica el patrón a cada segmento de la figura original. En esta sección analizaremos unas cuantas aproximaciones. [Nota: nos referiremos a nuestras figuras geométricas como fractales, aun cuando son aproximaciones]. Aunque estas figuras se han estudiado 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 auto-similar: cuando se subdividen en partes, cada una se asemeja a una copia del todo, en un tamaño reducido. Muchos fractales producen una copia exacta del original cuando se amplía una porción de la imagen original; se dice que dicho fractal es estrictamente auto-similar. En la sección 15.11 se proporcionan vínculos para diversos sitios Web en los que hay discusiones y demostraciones de los fractales. Como ejemplo, veamos un fractal popular, estrictamente auto-similar, conocido como la Curva de Koch (figura 15.15). 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 implican eliminar toda, o parte de,

www.elsolucionario.net

15.8

Figura 15.15 | Fractal Curva

Fractales

667

de Koch.

la imagen del fractal anterior. Este patrón ya se ha determinado para este fractal; en esta sección nos enfocaremos no sobre cómo determinar qué fórmulas se necesitan para un fractal específico, sino cómo utilizar esas fórmulas en una solución recursiva. Empezamos con una línea recta [figura 15.15, parte (a)] y aplicamos el patrón, creando un triángulo a partir de la tercera parte media [figura 15.15, parte (b)]. Después aplicamos el patrón de nuevo a cada línea recta, lo cual produce la figura 15.15, parte (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 fractales pueden mostrarse en muchos niveles; por ejemplo, a un fractal de nivel 3 se le han aplicado tres iteraciones del patrón [figura 15.15, partes (e y f )]. Como éste es un fractal estrictamente auto-similar, cada porción del mismo contiene una copia exacta del fractal. Por ejemplo, en la parte (f ) de la figura 15.15, hemos resaltado una porción del fractal con un cuadro color rojo 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 de la sección 15.11. 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 15.22). El caso base, o nivel 0 del fractal, empieza como una línea entre dos puntos, A y B (figura 15.16). Para crear el siguiente nivel superior, buscamos el punto medio (C) de la línea. Para calcular

www.elsolucionario.net

668

Capítulo 15

Recursividad

la ubicación del punto C, utilice la siguiente fórmula: [Nota: la x y la y a la izquierda de cada letra se refieren a las coordenadas x y y de ese punto, respectivamente. Por ejemplo, xA se refiere a la coordenada x del punto A, mientras que yC se refiere a la coordenada 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]. xC = (xA + xB) / 2; yC = (yA + yB) / 2;

Para crear este fractal, también debemos buscar un punto D que se encuentre a la izquierda del segmento 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;

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 15.17). Después se elimina la línea original y se agregan los segmentos DA, DC y DB. 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 15.18 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 han utilizado 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 15.18 y se sustituyen con nuevas líneas de los puntos C y D que se acaban de agregar. La figura 15.19 muestra las nuevas líneas (las líneas del nivel 2 se muestran como líneas punteadas, para conveniencia del lector). La figura 15.20 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 15.22. En breve presentaremos el código para esta aplicación. La aplicación de la figura 15.21 define la interfaz de usuario para dibujar este fractal (que se muestra al final de la figura 15.22). La interfaz consiste de tres botones: uno para que el usuario modifique el color 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 color predeterminado para dibujar el fractal será azul (línea 18). El usuario activa un evento ActionEvent haciendo clic en el botón Color. El manejador de eventos para este botón se registra en las líneas 38 a 54. El método actionPerformed muestra un cuadro de diálogo JColorChoo-

A (6, 5)

B (30, 5)

Origen (0, 0)

Figura 15.16 | El “fractal Lo” en el nivel 0.

www.elsolucionario.net

15.8

Fractales

D (12, 11)

A (6, 5)

C (18, 5)

B (30, 5)

Origen (0, 0)

Figura 15.17 | Determinación de los puntos C y D para el nivel 1 del “fractal Lo”.

D3 (18, 14) A (12, 11) C1 (9, 8) D1 (12, 8) B1 (6, 5)

D2 (15, 11) C2 (15, 8)

B2 (18, 5)

C3 (21, 8)

B3 (30, 5)

Origen (0, 0)

Figura 15.18 | 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].

Origen (0, 0)

Figura 15.19 | El “fractal Lo” en el nivel 2, y se proporcionan las líneas punteadas del nivel 1.

www.elsolucionario.net

669

670

Capítulo 15

Recursividad

Origen (0, 0)

Figura 15.20 | El “fractal Lo” en el nivel 2. ser. 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 51 se hace una llamada al método establecerColor en la clase FractalJPanel para actualizar el color. El manejador de eventos para el botón Reducir nivel se registra en las líneas 60 a 78. En el método actionPerformed, en las líneas 66 y 67 obtienen el nivel actual de recursividad y lo reducen en 1. En la línea 70 se

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

// Fig. 15.21: Fractal.java // Demuestra la interfaz de usuario para dibujar un fractal. import java.awt.Color; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JFrame; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JColorChooser; public class Fractal extends JFrame { private final int ANCHURA = 400; // define la anchura de la GUI private final int ALTURA = 480; // define la altura de la GUI private final int NIVEL_MIN = 0, NIVEL_MAX = 15; private Color color = Color.BLUE; private JButton cambiarColorJButton, aumentarNivelJButton, reducirNivelJButton; private JLabel nivelJLabel; private FractalJPanel espacioDibujo; private JPanel principalJPanel, controlJPanel; // establece la GUI public Fractal() { super( "Fractal" );

Figura 15.21 | Demostración de la interfaz de usuario del fractal. (Parte 1 de 3).

www.elsolucionario.net

15.8

31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88

Fractales

671

// establece el panel de control controlJPanel = new JPanel(); controlJPanel.setLayout( new FlowLayout() ); // establece el botón de color y registra el componente de escucha cambiarColorJButton = new JButton( "Color" ); controlJPanel.add( cambiarColorJButton ); cambiarColorJButton.addActionListener( new ActionListener() // clase interna anónima { // procesa el evento de cambiarColorJButton public void actionPerformed( ActionEvent evento ) { color = JColorChooser.showDialog( Fractal.this, "Elija un color", color ); // establece el color predeterminado, si no se devuelve un color if ( color == null ) color = Color.BLUE; espacioDibujo.establecerColor( color ); } // fin del método actionPerformed } // fin de la clase interna anónima ); // fin de addActionListener // establece botón para reducir nivel, para agregarlo al panel de control y // registra el componente de escucha reducirNivelJButton = new JButton( "Reducir nivel" ); controlJPanel.add( reducirNivelJButton ); reducirNivelJButton.addActionListener( new ActionListener() // clase interna anónima { // procesa el evento de reducirNivelJButton public void actionPerformed( ActionEvent evento ) { int nivel = espacioDibujo.obtenerNivel(); nivel--; // reduce el nivel en uno // modifica el nivel si es posible if ( ( nivel >= NIVEL_MIN ) && ( nivel = NIVEL_MIN ) && ( nivel ). Cada sección de parámetro de tipo contiene uno o más parámetros de tipo (también llamados parámetros de tipo formal), separados por comas. Un parámetro de tipo, también conocido como variable de tipo, es un identificador que especifica el nombre de un tipo genérico. Los parámetros de tipo se pueden utilizar para declarar el tipo de valor de retorno, los tipos de los parámetros y los tipos de las variables locales en la declaración de un método genérico, y actúan como receptáculos para los tipos de los argumentos que se pasan al método genérico, que conocemos como argumentos de tipos actuales. El cuerpo de un método genérico se declara como el de cualquier otro método. Observe que los parámetros de tipo sólo

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

// Fig. 18.3: PruebaMetodoGenerico.java // Uso de métodos genéricos para imprimir arreglos de distintos tipos. public class PruebaMetodoGenerico { // método genérico imprimirArreglo public static < E > void imprimirArreglo( E[] arregloEntrada ) { // muestra los elementos del arreglo for ( E elemento : arregloEntrada ) System.out.printf( "%s ", elemento ); System.out.println(); } // fin del método imprimirArreglo public static void main( String args[] ) { // crea arreglos de objetos Integer, Double y Character Integer[] arregloInteger = { 1, 2, 3, 4, 5, 6 }; Double[] arregloDouble = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7 }; Character[] arregloCharacter = { 'H', 'O', 'L', 'A' }; System.out.println( "El arreglo arregloInteger contiene:" ); imprimirArreglo( arregloInteger ); // pasa un arreglo Integer System.out.println( "\nEl arreglo arregloDouble contiene:" ); imprimirArreglo( arregloDouble ); // pasa un arreglo Double System.out.println( "\nEl arreglo arregloCharacter contiene:" ); imprimirArreglo( arregloCharacter ); // pasa un arreglo Character } // fin de main } // fin de la clase PruebaMetodoGenerico

El arreglo arregloInteger contiene: 1 2 3 4 5 6 El arreglo arregloDouble contiene: 1.1 2.2 3.3 4.4 5.5 6.6 7.7 El arreglo arregloCharacter contiene: H O L A

Figura 18.3 | Impresión de los elementos de un arreglo, usando el método genérico imprimirArreglo.

www.elsolucionario.net

766

Capítulo 18

Genéricos

pueden representar tipos por referencia, y no tipos primitivos (como int, double y char). Observe también que los nombres de los parámetros de tipo en la declaración del método deben coincidir con los que están declarados en la sección de parámetros de tipo. Por ejemplo, en la línea 10 se declara elemento como de tipo E, lo cual coincide con el parámetro de tipo (E) declarado en la línea 7. Además, un parámetro de tipo puede declararse sólo una vez en la sección de parámetros de tipo, pero puede aparecer más de una vez en la lista de parámetros del método. Por ejemplo, el nombre del parámetro de tipo E aparece dos veces en la siguiente lista de parámetros del método: public static < E > void imprimirDosArreglos( E[] arreglo1, E[] arreglo2 )

Los parámetros de tipo no necesitan ser únicos entre los distintos métodos genéricos.

Error común de programación 18.1 Al declarar un método genérico, si no se coloca una sección de parámetros de tipo antes del tipo de valor de retorno de un método, se produce un error de sintaxis; el compilador no comprenderá el nombre del parámetro de tipo cuando lo encuentre en el método.

La sección de parámetros de tipo del método imprimirArreglo declara el parámetro de tipo E como el receptáculo para el tipo de los elementos del arreglo que imprimirá imprimirArreglo. Observe que E aparece en la lista de parámetros como el tipo de los elementos del arreglo (línea 7). El encabezado de la instrucción for (línea 10) también utiliza a E como el tipo de los elementos. Éstas son las mismas dos ubicaciones en las que los métodos sobrecargados imprimirArreglo de la figura 18.1 especificaron a Integer, Double o Character como el tipo de los elementos del arreglo. El resto de imprimirArreglo es idéntico a las versiones que se presentan en la figura 18.1.

Buena práctica de programación 18.1 Se recomienda especificar los parámetros de tipo como letras mayúsculas individuales. Por lo general, un parámetro que representa el tipo de los elementos de un arreglo (o cualquier otra colección) se llama E, en representación de “elemento”.

Al igual que en la figura 18.1, el programa empieza por declarar e inicializar el arreglo Integer de seis elementos llamado arregloInteger (línea 19), el arreglo Double de siete elementos llamado arregloDouble (línea 20) y el arreglo Character de cuatro elementos llamado 7 (línea 21). Después el programa imprime cada arreglo mediante una llamada a imprimirArreglo (líneas 24, 26 y 28): una vez con el argumento arregloInteger, una vez con el argumento arregloDouble y una vez con el argumento arregloCharacter. Cuando el compilador encuentra la línea 24, primero determina el tipo del argumento de arregloInteger (es decir, Integer[]) y trata de localizar un método llamado imprimirArreglo, el cual especifica un solo parámetro Integer[]. No hay un método así en este ejemplo. Después, el compilador determina si hay un método genérico llamado imprimirArreglo, el cual especifica un solo parámetro para el arreglo y utiliza un parámetro de tipo para representar el tipo de los elementos del arreglo. El compilador determina que imprimirArreglo (líneas 7 a 14) es una coincidencia y establece una llamada a ese método. El mismo proceso se repite para las llamadas al método imprimirArreglo en las líneas 26 y 28.

Error común de programación 18.2 Si el compilador no puede relacionar una llamada a un método con una declaración de método no genérico o genérico, se produce un error de compilación.

Error común de programación 18.3 Si el compilador no encuentra la declaración de un método que coincida exactamente con una llamada a un método, pero encuentra dos o más métodos genéricos que puedan satisfacer la llamada a ese método, se produce un error de compilación.

Además de establecer las llamadas a los métodos, el compilador también determina si las operaciones en el cuerpo del método se pueden aplicar a los elementos del tipo almacenado en el argumento del arreglo. La única operación que se realiza con los elementos del arreglo en este ejemplo es imprimir la representación de cadena de los elementos. En la línea 11 se realiza una llamada implícita a toString en cada elemento. Para trabajar

www.elsolucionario.net

18.4

Cuestiones adicionales sobre la traducción en tiempo de compilación: métodos que utilizan...

767

con los genéricos, cada elemento del arreglo debe ser un objeto de una clase o tipo de interfaz. Como todos los objetos tienen un método toString, el compilador está satisfecho de que en la línea 11 se realice una operación válida para cualquier objeto en el argumento arreglo de imprimirArreglo. Los métodos toString de las clases Integer, Double y Character devuelven la representación de cadena del valor int, double o char subyacente, respectivamente. Cuando el compilador traduce el método genérico imprimirArreglo en códigos byte de Java, elimina la sección de parámetros de tipo y reemplaza los parámetros de tipo con tipos reales. A este proceso se le conoce como borrado. De manera predeterminada, todos los tipos genéricos se reemplazan con el tipo Object. Por lo tanto, la versión compilada del método imprimirArreglo aparece como se muestra en la figura 18.4; sólo hay una copia de este código que se utiliza para todas las llamadas a imprimirArreglo en el ejemplo. Esto es bastante distinto de otros mecanismo similares, como las plantillas de C++, en las cuales se genera y se compila una copia separada del código fuente para cada tipo que se pasa como argumento al método. Como veremos en la sección 18.4, la traducción y compilación de los genéricos es un proceso un poco más complicado de lo que hemos visto en esta sección. Al declarar a imprimirArreglo como método genérico en la figura 18.3, eliminamos la necesidad de los métodos sobrecargados de la figura 18.1, ahorrando 20 líneas de código y creando un método reutilizable que pueda imprimir las representaciones de cadena de los elementos en cualquier arreglo que contenga objetos. Sin embargo, este ejemplo en especial pudo haber declarado simplemente el método imprimirArreglo como se muestra en la figura 18.4, usando un arreglo Object como parámetro. Esto habría producido los mismos resultados, ya que cualquier objeto Object se puede imprimir como objeto String. En un método genérico, los beneficios se hacen presentes cuando el método también utiliza un parámetro de tipo como el tipo de valor de retorno del método, como lo demostraremos en la siguiente sección.

1 2 3 4 5 6 7 8

public static void imprimirArreglo( Object[] arregloEntrada ) { // muestra los elementos del arreglo for ( Object elemento : arregloEntrada ) System.out.printf( "%s ", elemento );

}

System.out.println(); // fin del método imprimirArreglo

Figura 18.4 | El método genérico imprimirArreglo, una vez que el compilador realiza el proceso de borrado.

18.4 Cuestiones adicionales sobre la traducción en tiempo de compilación: métodos que utilizan un parámetro de tipo como tipo de valor de retorno Consideremos un ejemplo de método genérico, en el cual se utilizan parámetros de tipo en el tipo de valor de retorno y en la lista de parámetros (figura 18.5). La aplicación utiliza un método genérico llamado maximo para determinar y devolver el mayor de sus tres argumentos del mismo tipo. Por desgracia, no se puede utilizar el operador relacional > con los tipos de referencia. Sin embargo, es posible comparar dos objetos de la misma clase, si esa clase implementa a la interfaz genérica Comparable< T > (paquete java.lang). Todas las clases de envoltura de tipos para los tipos primitivos implementan a esta interfaz. Al igual que las clases genéricas, las interfaces genéricas permiten a los programadores especificar, mediante la declaración de una sola interfaz, un conjunto de tipos relacionados. Los objetos Comparable< T > tienen un método llamado compareTo. Por ejemplo, si tenemos dos objetos Integer llamados entero1 y entero2, éstos pueden compararse con la siguiente expresión: entero1.compareTo( entero2 );

Es responsabilidad del programador encargado declarar una clase que implemente a Comparable< T > declarar el método compareTo, de tal forma que compare el contenido de dos objetos de esa clase y que devuelva los resultados de la comparación. El método debe devolver 0 si los objetos son iguales, -1 si objeto1 es menor que objeto2

www.elsolucionario.net

768

Capítulo 18

Genéricos

o 1 si objeto1 es mayor que objeto2. Por ejemplo, el método compareTo de la clase Integer compara los valores int almacenados en dos objetos Integer. Un beneficio de implementar la interfaz Comparable< T > es que pueden utilizarse objetos Comparable< T > con los métodos de ordenamiento y búsqueda de la clase Collections (paquete java.util). En el capítulo 19, Colecciones, hablaremos sobre esos métodos. En este ejemplo, utilizaremos el método compareTo en el método maximo para ayudar a determinar el valor más grande. El método genérico maximo (líneas 7 a 18) utiliza el parámetro T como el tipo de valor de retorno del método (línea 7), como el tipo de los parámetros x, y y z (línea 7) del método, y como el tipo de la variable local max (línea 9). La sección de parámetros de tipo especifica que T extiende a Comparable< T > (sólo pueden utilizarse objetos de clases que implementen a la interfaz Comparable< T > con este método). En este caso, Comparable se conoce como el límite superior del parámetro de tipo. De manera predeterminada, Object es el límite superior. Observe que las declaraciones de los parámetros de tipo que delimitan el parámetro siempre utilizan la palabra clave extends, sin importar que el parámetro de tipo extienda a una clase o implemente a una interfaz. Este parámetro de tipo es más restrictivo que el que se especifica para imprimirArreglo en la figura 18.3, el cual puede imprimir arreglos que contengan cualquier tipo de objeto. La restricción de usar objetos Comparable < T > es importante, ya que no todos los objetos se pueden comparar. Sin embargo, se garantiza que los objetos Comparable< T > tienen un método compareTo. El método maximo utiliza el mismo algoritmo que utilizamos en la sección 6.4 para determinar el mayor de sus tres argumentos. Este método asume que su primer argumento (x) es el mayor, y lo asigna a la variable local max (línea 9). A continuación, la instrucción if en las líneas 11 y 12 determina si y es mayor que max. La condición invoca al método compareTo de y con la expresión y.compareTo( max ), que devuelve -1, 0 o 1, para determinar la relación de y con max. Si el valor de retorno de compareTo es mayor que 0, entonces y es mayor y se asigna a la variable max. De manera similar, la instrucción if en las líneas 14 y 15 determina si z es mayor que max. Si es así, en la línea 15 se asigna z a max. Después. En la línea 17 se devuelve max al método que hizo la llamada.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

// Fig. 18.5: PruebaMaximo.java // El método genérico maximo devuelve el mayor de tres objetos. public class PruebaMaximo { // determina el mayor de tres objetos Comparable public static < T extends Comparable< T > > T maximo( T x, T y, T z ) { T max = x; // asume que x es el mayor, en un principio if ( y.compareTo( max ) > 0 ) max = y; // y es el mayor hasta ahora if ( z.compareTo( max ) > 0 ) max = z; // z es el mayor return max; // devuelve el objeto más grande } // fin del método maximo public static void main( String args[] ) { System.out.printf( "Maximo de %d, %d y %d es %d\n\n", 3, 4, 5, maximo( 3, 4, 5 ) ); System.out.printf( "Maximo de %.1f, %.1f y %.1f es %.1f\n\n", 6.6, 8.8, 7.7, maximo( 6.6, 8.8, 7.7 ) ); System.out.printf( "Maximo de %s, %s y %s es %s\n", "pera", "manzana", "naranja", maximo( "pera", "manzana", "naranja" ) ); } // fin de main } // fin de la clase PruebaMaximo

Figura 18.5 | El método genérico maximo, con un límite superior en su parámetro de tipo. (Parte 1 de 2).

www.elsolucionario.net

18.4

Cuestiones adicionales sobre la traducción en tiempo de compilación: métodos que utilizan...

769

Maximo de 3, 4 y 5 es 5 Maximo de 6.6, 8.8 y 7.7 es 8.8 Maximo de pera, manzana y naranja es pera

Figura 18.5 | El método genérico maximo, con un límite superior en su parámetro de tipo. (Parte 2 de 2). En main (líneas 20 a 28), en la línea 23 se hace una llamada a maximo con los enteros 3, 4 y 5. Cuando el compilador encuentra esta llamada, primero busca un método maximo que reciba tres argumentos de tipo int. No hay un método así, por lo cual el compilador busca un método genérico que pueda utilizarse, y encuentra el método genérico maximo. Sin embargo, recuerde que los argumentos para un método genérico deben ser de un tipo de referencia. Por lo tanto, el compilador realiza la conversión autoboxing de los tres valores int en objetos Integer, y especifica que estos tres objetos Integer se pasen a maximo. Observe que la clase Integer (paquete java.lang) implementa a la interfaz Comparable< Integer > de tal forma que el método compareTo compara los valores int en dos objetos Integer. Por lo tanto, los objetos Integer son argumentos válidos para el método maximo. Cuando se devuelve el objeto Integer que representa el máximo, tratamos de mostrarlo en pantalla con el especificador de formato %d, el cual muestra un valor del tipo primitivo int. Así, el valor de retorno de maximo se muestra en pantalla como un valor int. Hay un proceso similar que ocurre para los tres argumentos double que se pasan a maximo en la línea 25. Se realiza una conversión autoboxing en cada valor double para convertirlo en un objeto Double y pasarlo a maximo. De nuevo, esto se permite ya que la clase Double (paquete java.lang) implementa a la interfaz Comparable< Double >. El objeto Double devuelto por maximo se imprime en pantalla con el especificador de formato %.1f, el cual muestra un valor del tipo primitivo double. Así, se realiza una conversión autounboxing en el valor de retorno de maximo y se muestra en pantalla como un double. La llamada a maximo en la línea 27 recibe tres objetos String, que también son objetos Comparable< String >. Observe que colocamos de manera intencional el valor más grande en una posición distinta en cada llamada al método (líneas 23, 25 y 27), para mostrar que el método genérico siempre encuentra el valor máximo, sin importar su posición en la lista de argumentos. Cuando el compilador traduce el método genérico maximo en códigos byte de Java, utiliza el borrado (presentado en la sección 18.3) para reemplazar los parámetros de tipo con tipos reales. En la figura 18.3, todos los tipos genéricos se reemplazaron con el tipo Object. En realidad, todos los parámetros de tipo se reemplazan con el límite superior del parámetro de tipo; a menos que se especifique lo contrario, Object es el límite superior predeterminado. El límite superior de un parámetro de tipo se especifica en la sección de parámetros de tipo. Para indicar el límite superior, coloque después del nombre del parámetro de tipo la palabra clave extends y el nombre de la clase o interfaz que representa el límite superior. En la sección de parámetros de tipo del método maximo (figura 18.5), especificamos el límite superior como el tipo Comparable< T >. Por ende, sólo pueden pasarse objetos Comparable< T > como argumentos para maximo; cualquier cosa que no sea Comparable< T > producirá errores de compilación. La figura 18.6 simula el borrado de los tipos del método máximo, al mostrar el código fuente del método después de eliminar la sección de parámetros de tipo, y después de que se reemplaza

1 2 3 4 5 6 7 8 9 10 11 12

public static Comparable maximo(Comparable x, Comparable y, Comparable z) { Comparable max = x; // suponga que al principio x es el más grande if ( y.compareTo( max ) > 0 ) max = y; // y es el mayor hasta ahora if ( z.compareTo( max ) > 0 ) max = z; // z es el mayor return max; // devuelve el objeto más grande } // fin del método maximo

Figura 18.6 | El método genérico maximo, después de que el compilador realiza el borrado.

www.elsolucionario.net

770

Capítulo 18

Genéricos

el parámetro T con el límite superior, Comparable, en toda la declaración del método. Observe que el borrado de Comparable< T > es simplemente Comparable. Después del borrado, la versión compilada del método maximo especifica que devuelve el tipo Comparable. Sin embargo, el método que hace la llamada no espera recibir un objeto Comparable. En vez de ello, espera recibir un objeto del mismo tipo que se pasó a maximo como argumento: Integer, Double o String en este ejemplo. Cuando el compilador reemplaza la información del parámetro de tipo con el tipo del límite superior en la declaración del método, también inserta operaciones de conversión explícitas en frente de cada llamada al método, para asegurar que el valor devuelto sea del tipo esperado por el método que hizo la llamada. Así, la llamada a maximo en la línea 23 (figura 18.5) va antecedida por una conversión a Integer, como en (Integer) maximo( 3, 4, 5 )

la llamada a maximo en la línea 25 va antecedida por una conversión a Double, como en (Double) maximo( 6.6, 8.8, 7.7 )

y la llamada a maximo en la línea 27 va antecedida por una conversión a String, como en (String) maximo( "pera", "manzana", "naranja" )

En cada caso, el tipo de la conversión para el valor de retorno se infiere de los tipos de los argumentos del método en cada una de las llamadas al mismo, pues de acuerdo a la declaración del método, el tipo de valor de retorno y los tipos de los argumentos coinciden. En este ejemplo no podemos usar un método que acepte objetos Object, ya que la clase Object sólo cuenta con una comparación de igualdad. Además, sin los genéricos nosotros seríamos responsables de implementar la operación de conversión. El uso de genéricos asegura que la conversión insertada nunca lance una excepción ClassCastException, asumiendo que utilizamos genéricos en nuestro código (es decir, no debemos mezclar el código anterior con el nuevo código de genéricos).

18.5 Sobrecarga de métodos genéricos Un método genérico puede sobrecargarse. Una clase puede proporcionar dos o más métodos genéricos que especifiquen el mismo nombre del método, pero distintos parámetros. Por ejemplo, el método genérico imprimirArreglo de la figura 18.3 podría sobrecargarse con otro método genérico imprimirArreglo con los parámetros adicionales subindiceInferior y subindiceSuperior, para especificar la parte del arreglo a imprimir (vea el ejercicio 18.5). Un método genérico también puede sobrecargarse mediante métodos no genéricos que tengan el mismo nombre del método y el mismo número de parámetros. Cuando el compilador encuentra una llamada al método, busca la declaración del método que coincida con más precisión con el nombre del método y los tipos de los argumentos especificados en la llamada. Por ejemplo, el método genérico imprimirArreglo de la figura 18.3 podría sobrecargarse con una versión específica para objetos String, que imprima estos objetos en un impecable formato tabular (vea el ejercicio 18.6). Cuando el compilador encuentra una llamada al método, realiza un proceso de asociación para determinar cuál método debe invocar. El compilador trata de encontrar y utilizar una coincidencia precisa, en la que los nombres y tipos de los argumentos de la llamada al método coincidan con los de una declaración específica de ese método. Si no hay un método así, el compilador determina si hay un método inexacto que coincida, y que pueda aplicarse.

18.6 Clases genéricas El concepto de una estructura de datos, como una pila, puede comprenderse en forma independiente del tipo de elemento que manipula. Las clases genéricas proporcionan los medios para describir el concepto de una pila (o cualquier otra clase) en forma independiente de su tipo. Así, podemos crear instancias de objetos con tipos específicos de la clase genérica. Esta capacidad ofrece una maravillosa oportunidad para la reutilización de software. Una vez que tenemos una clase genérica, podemos usar una notación concisa para indicar el (los) tipo(s) actual(es) que debe(n) usarse en lugar del (los) parámetro(s) de tipo de la clase. En tiempo de compilación, el compilador de Java asegura la seguridad de los tipos de nuestro código, y utiliza las técnicas de borrado descritas en las secciones 18.3 y 18.4 para permitir que nuestro código cliente interactúe con la clase genérica.

www.elsolucionario.net

18.6

Clases genéricas

771

Por ejemplo, una clase Pila genérica podría ser la base para crear muchas clases de Pila (como, “Pila de de Integer”, “Pila de Character”, “Pila de Empleado”). Estas clases se conocen como clases parametrizadas o tipos parametrizados, ya que aceptan uno o más parámetros. Recuerde que los parámetros de tipo sólo representan a los tipos de referencias, lo cual significa que la clase genérica Pila no puede instanciarse con tipos primitivos. Sin embargo, podemos instanciar una Pila que almacene objetos de las clases de envoltura de tipos de Java, y permitir que Java utilice la conversión autoboxing para convertir los valores primitivos en objetos. La conversión autoboxing ocurre cuando un valor de un tipo primitivo (por ejemplo, int) se mete en una Pila que contiene objetos de clases de envoltura (como, Integer). La conversión autounboxing ocurre cuando un objeto de la clase de envoltura se saca de la Pila y se asigna a una variable de tipo primitivo. En la figura 18.7 se presenta una declaración de la clase genérica Pila. La declaración de una clase genérica es similar a la de una clase no genérica, excepto que el nombre de la clase va seguido de una sección de parámetros de tipo (línea 4). En este caso, el parámetro de tipo E el tipo del elemento que manipulará la Pila. Al igual que con los métodos genéricos, la sección de parámetros de tipo de una clase genérica puede tener uno o más parámetros separados por comas. (Usted creará una clase genérica con dos parámetros de tipo en el ejercicio 18.8). El parámetro de tipo E se utiliza en la declaración de la clase Pila para representar el tipo del elemento. [Nota: este ejemplo implementa una Pila como un arreglo]. Double”, “Pila

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

// Fig. 18.7: Pila.java // La clase genérica Pila. public class Pila< E > { private final int tamanio; // número de elementos en la pila private int superior; // ubicación del elemento superior private E[] elementos; // arreglo que almacena los elementos de la pila // el constructor sin argumentos crea una pila del tamaño predeterminado public Pila() { this( 10 ); // tamaño predeterminado de la pila } // fin del constructor de Pila sin argumentos // constructor que crea una pila del número especificado de elementos public Pila( int s ) { tamanio = s > 0 ? s : 10; // establece el tamaño de la Pila superior = -1; // al principio, la Pila está vacía elementos = ( E[] ) new Object[ tamanio ]; // crea el arreglo } // fin del constructor de Pila sin argumentos // mete un elemento a la pila; si tiene éxito, devuelve verdadero; // en caso contrario, lanza excepción ExcepcionPilaLlena public void push( E valorAMeter ) { if ( superior == tamanio - 1 ) // si la pila está llena throw new ExcepcionPilaLlena( String.format( "La Pila esta llena, no se puede meter %s", valorAMeter ) ); elementos[ ++superior ] = valorAMeter; // coloca valorAMeter en la Pila } // fin del método push // devuelve el elemento superior si no está vacía; de lo contrario lanza ExcepcionPilaVacia public E pop()

Figura 18.7 | Declaración de la clase genérica Pila. (Parte 1 de 2).

www.elsolucionario.net

772

38 39 40 41 42 43 44

Capítulo 18

Genéricos

{ if ( superior == -1 ) // si la pila está vacía throw new ExcepcionPilaVacia( "La Pila esta vacia, no se puede sacar" ); return elementos[ superior-- ]; // elimina y devuelve el elemento superior de la Pila } // fin del método pop } // fin de la clase Pila< E >

Figura 18.7 | Declaración de la clase genérica Pila. (Parte 2 de 2).

La clase Pila declara la variable elementos como un arreglo de tipo E (línea 8). Este arreglo almacenará los elementos de la Pila. Nos gustaría crear un arreglo de tipo E para almacenar los elementos. Sin embargo, el mecanismo de los genéricos no permite parámetros de tipo en las expresiones para crear arreglos, debido a que el parámetro de tipo (en este caso, E) no está disponible en tiempo de ejecución. Para crear un arreglo con el tipo apropiado, en la línea 22 del constructor sin argumentos se crea el arreglo como un arreglo de tipo Object y se convierte la referencia devuelta por new al tipo E[]. Cualquier objeto podría almacenarse en un arreglo Object, pero el mecanismo de comprobación de tipos del compilador asegura que sólo puedan asignarse al arreglo objetos del tipo declarado de la variable arreglo, a través de cualquier expresión de acceso a arreglos que utilice la variable elementos. Aun así, cuando se compila esta clase usando la opción –Xlint:unchecked, por ejemplo: javac –Xlint:unchecked Pila.java

el compilador emite el siguiente mensaje de advertencia acerca de la línea 22: Stack.java:22: warning: [unchecked] unchecked cast found : java.langObject[] required : E[] elementos = ( E[] ) new Object[ tamanio ]; // crea el arreglo

La razón de este mensaje es que el compilador no puede asegurar con un 100% de certeza que un arreglo de tipo Object nunca contendrá objetos de tipos que no sean E. Suponga que E representa el tipo Integer, de manera que los elementos del arreglo deben almacenar objetos Integer. Es posible asignar la variable elementos a una variable de tipo Object[], como en Object[] arregloObjetos = elementos;

Entonces, cualquier objeto puede colocarse en el arreglo con una instrucción de asignación tal como arregloObjetos[ 0 ] = "hola";

Esto coloca un objeto String en un arreglo que debe contener sólo objetos Integer, lo cual produciría problemas en tiempo de compilación al manejar la Pila. Mientras que no ejecute instrucciones como las que se muestran aquí, su Pila sólo contendrá objetos del tipo de elemento correcto. El método push (líneas 27 a 34) determina primero si hay un intento de meter un elemento en una Pila llena. De ser así, en las líneas 30 y 31 se lanza una ExcepcionPilaLlena. La clase ExcepcionPilaLlena se declara en la figura 18.8. Si la Pila no está llena, en la línea 33 se incrementa el contador superior y se coloca el argumento en esa ubicación del arreglo elementos. El método pop (líneas 37 a 43) determina primero si hay un intento de sacar un elemento de una Pila vacía. De ser así, en la línea 40 se lanza una ExcepcionPilaVacia. La clase ExcepcionPilaVacia se declara en la figura 18.9. En caso contrario, en la línea 42 se devuelve el elemento superior de la Pila, y después se postdecrementa el contador superior para indicar la posición del siguiente elemento superior. Cada una de las clases ExcepcionPilaLlena (figura 18.8) y ExcepcionPilaVacia (figura 18.9) proporciona el constructor sin argumentos convencional, y un constructor con un argumento de clases de excepciones (como vimos en la sección 13.11). El constructor sin argumentos establece el mensaje de error predeterminado, y el constructor con un argumento establece un mensaje de excepción personalizado. Al igual que con los métodos genéricos, cuando se compila una clase genérica, el compilador realiza el borrado en los parámetros de tipo de la clase y los reemplaza con sus límites superiores. Para la clase Pila (figura 18.7)

www.elsolucionario.net

18.6

Clases genéricas

773

no se especifica un límite superior, por lo que se utiliza el límite superior predeterminado, Object. El alcance de un parámetro de tipo de una clase genérica es toda la clase. Sin embargo, los parámetros de tipo no se pueden utilizar en las declaraciones static de una clase.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

// Fig. 18.8: ExcepcionPilaLlena.java // Indica que una pila está llena. public class ExcepcionPilaLlena extends RuntimeException { // constructor sin argumentos public ExcepcionPilaLlena() { this( "La Pila esta llena" ); } // fin del constructor de ExcepcionPilaLlena sin argumentos // constructor con un argumento public ExcepcionPilaLlena( String excepcion ) { super( excepcion ); } // fin del constructor de ExcepcionPilaLlena sin argumentos } // fin de la clase ExcepcionPilaLlena

Figura 18.8 | Declaración de la clase ExcepcionPilaLlena.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

// Fig. 18.9: ExcepcionPilaVacia.java // Indica que una pila está llena. public class ExcepcionPilaVacia extends RuntimeException { // constructor sin argumentos public ExcepcionPilaVacia() { this( "La Pila esta vacia" ); } // fin del constructor de ExcepcionPilaVacia sin argumentos // constructor con un argumento public ExcepcionPilaVacia( String excepcion ) { super( excepcion ); } // fin del constructor de ExcepcionPilaVacia con un argumento } // fin de la clase ExcepcionPilaVacia

Figura 18.9 | Declaración de la clase ExcepcionPilaVacia. Ahora consideremos la aplicación de prueba (figura 18.10) que utiliza la clase genérica Pila. En las líneas 9 y 10 se declaran variables de tipo Pila< Double > (lo cual se pronuncia como “Pila de Double”) y Pila< Integer > (que se pronuncia como “Pila de Integer”). Los tipos Double e Integer se conocen como los argumentos de tipo de la Pila. El compilador los utiliza para reemplazar los parámetros de tipo, de manera que pueda realizar la comprobación de tipos e insertar operaciones de conversión según sea necesario. En breve hablaremos con más detalle sobre las operaciones de conversión. El método probarPila (que se llama desde main) instancia objetos pilaDouble de tamaño 5 (línea 15) y pilaInteger de tamaño 10 (línea 16), y después llama a los métodos pruebaPushDouble (líneas 25 a 44), pruebaPopDouble (líneas 47 a 67), pruebaPushInteger (líneas 70 a 89) y pruebaPopInteger (líneas 92 a 112), para demostrar las dos Pilas en este ejemplo. El método pruebaPushDouble (líneas 25 a 44) invoca al método push para colocar en pilaDouble los valores double 1.1, 2.2, 3.3, 4.4 y 5.5 que se almacenan en el arreglo elementosDouble. El ciclo for termina cuando el programa de prueba trata de meter un sexto valor en pilaDouble (que está llena, ya que pilaDouble sólo puede almacenar cinco elementos). En este caso, el método lanza una ExcepcionPilaLlena (figura 18.8)

www.elsolucionario.net

774

Capítulo 18

Genéricos

para indicar que la Pila está llena. En las líneas 39 a 43 se atrapa esta excepción y se imprime la información de rastreo de la pila. Esta información indica la excepción que ocurrió y muestra que el método push de Pila generó la excepción en las líneas 30 y 31 del archivo Pila.java (figura 18.7). El rastreo también muestra que el método pruebaPushDouble de PruebaPila llamó al método push en la línea 36 de PruebaPila.java, que el método pruebaPilas llamó al método probarPushDouble en la línea 18 de PruebaPila.java y que el método main llamó al método pruebaPilas en la línea 117 de PruebaPila.java. Esta información nos permite determinar los métodos que se encontraban en la pila de llamadas a métodos cuando ocurrió la excepción. Debido a que el programa atrapa a la excepción, el entorno en tiempo de ejecución de Java considera que ha sido manejada la excepción y el programa puede continuar su ejecución. Observe que la conversión autoboxing ocurre en la línea 36, cuando el programa trata de meter un valor primitivo double en la pilaDouble, la cual sólo almacena objetos Double.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

// Fig. 18.10: PruebaPila.java // Programa de prueba de la clase genérica Pila. public class PruebaPila { private double[] elementosDouble = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6 }; private int[] elementosInteger = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; private Pila< Double > pilaDouble; // pila que almacena objetos Double private Pila< Integer > pilaInteger; // pila que almacena objetos Integer // prueba objetos Pila public void pruebaPilas() { pilaDouble = new Pila< Double >( 5 ); // Pila de objetos Double pilaInteger = new Pila< Integer >( 10 ); // Pila de objetos Integer pruebaPushDouble(); // mete valor double en pilaDouble pruebaPopDouble(); // saca de pilaDouble pruebaPushInteger(); // mete valor int en pilaInteger pruebaPopInteger(); // saca de pilaInteger } // fin del método probarPilas // prueba el método push con la pila de valores double public void pruebaPushDouble() { // mete elementos en la pila try { System.out.println( "\nMetiendo elementos en pilaDouble" ); // mete elementos en la Pila for ( double elemento : elementosDouble ) { System.out.printf( "%.1f ", elemento ); pilaDouble.push( elemento ); // mete en pilaDouble } // fin de for } // fin de try catch ( ExcepcionPilaLlena excepcionPilaLlena ) { System.err.println(); excepcionPilaLlena.printStackTrace(); } // find de catch ExcepcionPilaLlena } // fin del método pruebaPushDouble

Figura 18.10 | Programa de prueba de la clase genérica Pila. (Parte 1 de 3).

www.elsolucionario.net

18.6

45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103

Clases genéricas

// prueba el método pop con una pila de valores double public void pruebaPopDouble() { // saca elementos de la pila try { System.out.println( "\nSacando elementos de pilaDouble" ); double valorASacar; // almacena el elemento que se eliminó de la pila // elimina todos los elementos de la Pila while ( true ) { valorASacar = pilaDouble.pop(); // saca de pilaDouble System.out.printf( "%.1f ", valorASacar ); } // fin de while } // fin de try catch( ExcepcionPilaVacia excepcionPilaVacia ) { System.err.println(); excepcionPilaVacia.printStackTrace(); } // fin de catch ExcepcionPilaVacia } // fin del método pruebaPopDouble // prueba el método push con pila de valores enteros public void pruebaPushInteger() { // mete elementos a la pila try { System.out.println( "\nMetiendo elementos a pilaInteger" ); // mete elementos a la Pila for ( int elemento : elementosInteger ) { System.out.printf( "%d ", elemento ); pilaInteger.push( elemento ); // mete a pilaInteger } // fin de for } // fin de try catch ( ExcepcionPilaLlena excepcionPilaLlena ) { System.err.println(); excepcionPilaLlena.printStackTrace(); } // fin de catch ExcepcionPilaLlena } // fin del método pruebaPushInteger // prueba el método pop con una pila de enteros public void pruebaPopInteger() { // saca elementos de la pila try { System.out.println( "\nSacando elementos de pilaInteger" ); int valorASacar; // almacena el elemento que se eliminó de la pila // elimina todos los elementos de la Pila while ( true ) { valorASacar = pilaInteger.pop(); // saca de pilaInteger

Figura 18.10 | Programa de prueba de la clase genérica Pila. (Parte 2 de 3).

www.elsolucionario.net

775

776

104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119

Capítulo 18

Genéricos

System.out.printf( "%d ", valorASacar ); } // fin de while } // fin de try catch( ExcepcionPilaVacia excepcionPilaVacia ) { System.err.println(); excepcionPilaVacia.printStackTrace(); } // fin de catch ExcepcionPilaVacia } // fin del método pruebaPopInteger public static void main( String args[] ) { PruebaPila aplicacion = new PruebaPila(); aplicacion.probarPilas(); } // fin de main } // fin de la clase PruebaPila

Metiendo elementos en pilaDouble 1.1 2.2 3.3 4.4 5.5 6.6 ExcepcionPilaLlena: La Pila esta llena, no se puede meter 6.6 at Pila.push(Pila.java:30) at PruebaPila.pruebaPushDouble(PruebaPila.java:36) at PruebaPila.probarPilas(PruebaPila.java:18) at PruebaPila.main(PruebaPila.java:117) Sacando elementos de pilaDouble 5.5 4.4 3.3 2.2 1.1 ExcepcionPilaVacia: La Pila esta vacia, no se puede sacar at Pila.pop(Pila.java:40) at PruebaPila.pruebaPopDouble(PruebaPila.java:58) at PruebaPila.probarPilas(PruebaPila.java:19) at PruebaPila.main(PruebaPila.java:117) Metiendo elementos a pilaInteger 1 2 3 4 5 6 7 8 9 10 11 ExcepcionPilaLlena: La Pila esta llena, no se puede meter 11 at Pila.push(Pila.java:30) at PruebaPila.pruebaPushInteger(PruebaPila.java:81) at PruebaPila.probarPilas(PruebaPila.java:20) at PruebaPila.main(PruebaPila.java:117) Sacando elementos de pilaInteger 10 9 8 7 6 5 4 3 2 1 ExcepcionPilaVacia: La Pila esta vacia, no se puede sacar at Pila.pop(Pila.java:40) at PruebaPila.pruebaPopInteger(PruebaPila.java:103) at PruebaPila.probarPilas(PruebaPila.java:21) at PruebaPila.main(PruebaPila.java:117)

Figura 18.10 | Programa de prueba de la clase genérica Pila. (Parte 3 de 3). El método pruebaPopDouble (líneas 47 a 67) invoca al método pop de Pila en un ciclo while infinito para eliminar todos los valores de la pila. Observe en los resultados que los valores se sacan sin duda en el orden último en entrar, primero en salir (desde luego que ésta es la característica que define a las pilas). El ciclo while (líneas 57 a 61) continúa hasta que la pila está vacía (es decir, hasta que ocurre una ExcepcionPilaVacia), lo cual hace que el programa continúe con el bloque catch (líneas 62 a 66) y maneje la excepción, para que pueda continuar su ejecución. Cuando el programa de prueba trata de sacar un sexto valor, la pilaDouble está vacía, por lo que el método pop lanza una ExcepcionPilaVacia. La conversión autoboxing ocurre en la línea 58, en donde el programa asigna el objeto Double que se sacó de la pila a una variable primitiva double. En la sección 18.4 vimos

www.elsolucionario.net

18.6

Clases genéricas

777

que el compilador inserta operaciones de conversión para asegurar que se devuelvan los tipos apropiados de los métodos genéricos. Después del borrado, el método pop de Pila devuelve el tipo Object. Sin embargo, el código cliente en el método pruebaPopDouble espera recibir un valor double cuando regresa el método pop. Así, el compilador inserta una conversión a Double, como en valorASacar = ( Double ) pilaDouble.pop();

para asegurar que se devuelva una referencia del tipo apropiado, que se realice la conversión autounboxing y se asigne a valorASacar. El método pruebaPushInteger (líneas 70 a 89) invoca el método push de Pila para colocar valores en PruebaInteger hasta que esté llena. El método pruebaPopInteger (líneas 92 a 112) invoca el método pop de Prueba para eliminar valores de pilaInteger hasta que esté vacía. Una vez más, observe que los valores se sacan en el orden último en entrar, primero en salir. Durante el proceso de borrado, el compilador reconoce que el código cliente en el método pruebaPopInteger espera recibir un valor int cuando regresa el método pop. Por lo tanto, el compilador inserta una conversión a Integer, como en valorASacar = ( Integer ) pilaInteger.pop();

para asegurar que se devuelva una referencia del tipo apropiado, se realice una conversión autounboxing y se asigne a valorASacar.

Creación de métodos genéricos para probar la clase Pila< E > Observe que el código en los métodos pruebaPushDouble y pruebaPushInteger es casi idéntico para meter valores en una Pila o una Pila, respectivamente, y que el código en los métodos pruebaPopDouble y pruebaPopInteger es casi idéntico para sacar valores de una Pila o una Pila, respectivamente. Esto presenta otra oportunidad para utilizar los métodos genéricos. En la figura 18.11 se declara el método genérico probarPush (líneas 26 a 46) para que realice las mismas tareas que pruebaPushDouble y pruebaPushInteger en la figura 18.10; es decir, meter valores en una Pila< T >. De manera similar, el método genérico pruebaPop (líneas 49 a 69) realiza las mismas tareas que pruebaPopDouble y pruebaPopInteger en la figura 18.10; es decir, sacar valores de una Pila< T >. Observe que la salida de la figura 18.11 coincide precisamente con la salida de la figura 18.10. El método probarPilas (líneas 14 a 23) crea los objetos Pila< Double > (línea 16) y la Pila< Integer > (línea 17). En las líneas 19 a 22 se invocan los métodos genéricos pruebaPush y pruebaPop para probar los objetos Pila. Recuerde que los parámetros de tipo sólo pueden representar tipos de referencias. Por lo tanto, para poder pasar los arreglos elementosDouble y elementosInteger al método genérico pruebaPush, los arreglos declarados en las líneas 6 a 8 deben declararse con los tipos de envoltura Double e Integer. Cuando estos arreglos se inicializan con valores primitivos, el compilador realiza conversiones autoboxing en cada valor primitivo.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

// Fig. 18.11: PruebaPila2.java // Programa de prueba de la clase genérica Pila. public class PruebaPila2 { private Double[] elementosDouble = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6 }; private Integer[] elementosInteger = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; private Pila< Double > pilaDouble; // pila que almacena objetos Double private Pila< Integer > pilaInteger; // pila que almacena objetos Integer // prueba los objetos Pila public void probarPilas() { pilaDouble = new Pila< Double >( 5 ); // Pila de objetos Double pilaInteger = new Pila< Integer >( 10 ); // Pila de objetos Integer

Figura 18.11 | Paso de una Pila de tipo genérico a un método genérico. (Parte 1 de 3).

www.elsolucionario.net

778

18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76

Capítulo 18

Genéricos

probarPush( "pilaDouble", pilaDouble, elementosDouble ); probarPop( "pilaDouble", pilaDouble ); probarPush( "pilaInteger", pilaInteger, elementosInteger ); probarPop( "pilaInteger", pilaInteger ); } // fin del método probarPilas // el método genérico probarPush mete elementos en una Pila public < T > void probarPush( String nombre, Pila< T > pila, T[] elementos ) { // mete elementos a la pila try { System.out.printf( "\nMetiendo elementos a %s\n", nombre ); // mete elementos a la Pila for ( T elemento : elementos ) { System.out.printf( "%s ", elemento ); pila.push( elemento ); // mete elemento a la pila } } // fin de try catch ( ExcepcionPilaLlena excepcionPilaLlena ) { System.out.println(); excepcionPilaLlena.printStackTrace(); } // fin de catch ExcepcionPilaLlena } // fin del método probarPush // el método genérico probarPop saca elementos de una Pila public < T > void probarPop( String nombre, Pila< T > pila ) { // saca elementos de la pila try { System.out.printf( "\nSacando elementos de %s\n", nombre ); T valorASacar; // almacena el elemento eliminado de la pila // elimina todos los elementos de la Pila while ( true ) { valorASacar = pila.pop(); // saca de la pila System.out.printf( "%s ", valorASacar ); } // fin de while } // fin de try catch( ExcepcionPilaVacia excepcionPilaVacia ) { System.out.println(); excepcionPilaVacia.printStackTrace(); } // fin de catch ExcepcionPilaVacia } // fin del método probarPop public static void main( String args[] ) { PruebaPila2 aplicacion = new PruebaPila2(); aplicacion.probarPilas(); } // fin de main } // fin de la clase PruebaPila2

Figura 18.11 | Paso de una Pila de tipo genérico a un método genérico. (Parte 2 de 3).

www.elsolucionario.net

18.7

Tipos crudos (raw)

779

Metiendo elementos a pilaDouble 1.1 2.2 3.3 4.4 5.5 6.6 ExcepcionPilaLlena: La Pila esta llena, no se puede meter 6.6 at Pila.push(Pila.java:30) at PruebaPila2.probarPush(PruebaPila2.java:38) at PruebaPila2.probarPilas(PruebaPila2.java:19) at PruebaPila2.main(PruebaPila2.java:74) Sacando elementos de pilaDouble 5.5 4.4 3.3 2.2 1.1 ExcepcionPilaVacia: La Pila esta vacia, no se puede sacar at Pila.pop(Pila.java:40) at PruebaPila2.probarPop(PruebaPila2.java:60) at PruebaPila2.probarPilas(PruebaPila2.java:20) at PruebaPila2.main(PruebaPila2.java:74) Metiendo elementos a pilaInteger 1 2 3 4 5 6 7 8 9 10 11 ExcepcionPilaLlena: La Pila esta llena, no se puede meter 11 at Pila.push(Pila.java:30) at PruebaPila2.probarPush(PruebaPila2.java:38) at PruebaPila2.probarPilas(PruebaPila2.java:21) at PruebaPila2.main(PruebaPila2.java:74) Sacando elementos de pilaInteger 10 9 8 7 6 5 4 3 2 1 ExcepcionPilaVacia: La Pila esta vacia, no se puede sacar at Pila.pop(Pila.java:40) at PruebaPila2.probarPop(PruebaPila2.java:60) at PruebaPila2.probarPilas(PruebaPila2.java:22) at PruebaPila2.main(PruebaPila2.java:74)

Figura 18.11 | Paso de una Pila de tipo genérico a un método genérico. (Parte 3 de 3). El método genérico probarPush (líneas 26 a 46) usa el parámetro de tipo T (especificado en la línea 26) para representar el tipo de datos almacenado en la Pila< T >. El método genérico recibe tres argumentos: un String que representa el nombre del objeto Pila< T > para fines de mostrarlo en pantalla, una referencia a un objeto de tipo Pila< T > y un arreglo de tipo T; el tipo de elementos que se meterán en la Pila< T >. Observe que el compilador hace valer la consistencia entre el tipo de la Pila y los elementos que se meterán en la misma cuando se invoque a push, lo cual es el valor real de la llamada al método genérico. El método genérico probarPop (líneas 49 a 69) recibe dos argumentos: un String que represente el nombre del objeto Pila< T > para fines de mostrarlo en pantalla, y una referencia a un objeto de tipo Pila< T >.

18.7 Tipos crudos (raw) Los programas de prueba para la clase genérica Pila en la sección 18.6 crea instancias de objetos Pila con los argumentos de tipo Double e Integer. También es posible instanciar la clase genérica Pila sin especificar un argumento de tipo, como se muestra a continuación: Stack pilaObjetos = new Stack( 5 ); // no se especifica un argumento de tipo

En este caso, se dice que la pilaObjetos tiene un tipo crudo, lo cual significa que el compilador utiliza de manera implícita el tipo Object en la clase genérica para cada argumento de tipo. Así, la instrucción anterior crea una Pila que puede almacenar objetos de cualquier tipo. Esto es importante para la compatibilidad inversa con versiones anteriores de Java. Por ejemplo, todas las estructuras de datos del Marco de trabajo Collections de Java (vea el capítulo 19, Colecciones) almacenan referencias a objetos Object, pero ahora se implementan como tipos genéricos.

www.elsolucionario.net

780

Capítulo 18

Genéricos

A una variable Pila de tipo crudo se le puede asignar una Pila que especifique un argumento de tipo, como un objeto Pila< Double >, de la siguiente manera: Pila

pilaTipoCrudo2 = new Pila< Double >( 5 );

debido a que el tipo Double es una subclase de Object. La asignación se permite ya que los elementos en una Pila< Double > (es decir, objetos Double) son ciertamente objetos; la clase Double es una subclase indirecta de Object. De manera similar, a una variable Pila que especifica a un argumento de tipo en su declaración se le puede asignar un objeto Pila de tipo crudo, como en: Pila< Integer > pilaInteger = new Pila( 10 );

Aunque esta asignación está permitida, no es segura debido a que una Pila de tipo crudo podría almacenar tipos distintos de Integer. En este caso, el compilador genera un mensaje de advertencia, el cual indica la asignación insegura. El programa de prueba de la figura 18.12 utiliza la noción de un tipo crudo. En la línea 14 se crea una instancia de la clase genérica Pila con un tipo crudo, lo cual indica que pilaTipoCrudo1 puede contener objetos de cualquier tipo. En la línea 17 se asigna una Pila< Double > a la variable pilaTipoCrudo2, la cual se declara como una Pila de tipo crudo. En la línea 20 se asigna una Pila de tipo crudo a la variable Pila< Integer >, lo cual es legal pero hace que el compilador genere un mensaje de advertencia (figura 18.13), indicando una asignación potencialmente insegura; de nuevo, esto ocurre debido a que una Pila de tipo crudo podría almacenar tipos distintos de Integer. Además, cada una de las llamadas al método genérico probarPush y probarPop en las líneas 22 a 25 produce un mensaje de advertencia del compilador (figura 18.13). Estas advertencias ocurren debido a que las variables pilaTipoCrudo1 y pilaTipoCrudo2 se declaran como Pilas de tipo crudo, pero los métodos probarPush y probarPop esperan un segundo argumento que sea una Pila con un argumento de tipo específico. Las advertencias indican que el compilador no puede garantizar que los tipos manipulados por las pilas sean los correctos, ya que no suministramos una variable declarada con un argumento de tipo. Los métodos probarPush (líneas 31 a 51) y probarPop (líneas 54 a 74) son iguales que en la figura 18.11.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

// Fig. 18.12: PruebaTipoCrudo.java // Programa de prueba de tipos crudos. public class PruebaTipoCrudo { private Double[] elementosDouble = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6 }; private Integer[] elementosInteger = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; // método para evaluar Pilas con tipos crudos public void probarPilas() { // Pila de tipos crudos asignada a una variable Pila de tipos crudos Pila pilaTipoCrudo1 = new Pila( 5 ); // Pila< Double > asignada a una variable Pila de tipos crudos Pila pilaTipoCrudo2 = new Pila< Double >( 5 ); // Pila de tipos crudos asignada a una variable Pila< Integer > Pila< Integer > pilaInteger = new Pila( 10 ); probarPush( "pilaTipoCrudo1", pilaTipoCrudo1, elementosDouble ); probarPop( "pilaTipoCrudo1", pilaTipoCrudo1 ); probarPush( "pilaTipoCrudo2", pilaTipoCrudo2, elementosDouble ); probarPop( "pilaTipoCrudo2", pilaTipoCrudo2 ); probarPush( "pilaInteger", pilaInteger, elementosInteger );

Figura 18.12 | Programa de prueba de tipos crudos. (Parte 1 de 3).

www.elsolucionario.net

18.7

27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81

Tipos crudos (raw)

probarPop( "pilaInteger", pilaInteger ); } // fin del método probarPilas // método genérico que mete elementos a la pila public < T > void probarPush( String nombre, Pila< T > pila, T[] elementos ) { // mete elementos a la pila try { System.out.printf( "\nMetiendo elementos a %s\n", nombre ); // mete elementos a la Pila for ( T elemento : elementos ) { System.out.printf( "%s ", elemento ); pila.push( elemento ); // mete elemento a la pila } // fin de for } // fin de try catch ( ExcepcionPilaLlena excepcionPilaLlena ) { System.out.println(); excepcionPilaLlena.printStackTrace(); } // fin de catch ExcepcionPilaLlena } // fin del método probarPush // método genérico probarPop para sacar elementos de la pila public < T > void probarPop( String nombre, Pila< T > pila ) { // saca elementos de la pila try { System.out.printf( "\nSacando elementos de %s\n", nombre ); T valorASacar; // almacena el elemento eliminado de la pila // elimina elementos de la Pila while ( true ) { valorASacar = pila.pop(); // saca de la pila System.out.printf( "%s ", valorASacar ); } // fin de while } // fin de try catch( ExcepcionPilaVacia excepcionPilaVacia ) { System.out.println(); excepcionPilaVacia.printStackTrace(); } // fin de catch ExcepcionPilaVacia } // fin del método probarPop public static void main( String args[] ) { PruebaTipoCrudo aplicacion = new PruebaTipoCrudo(); aplicacion.probarPilas(); } // fin de main } // fin de la clase PruebaTipoCrudo

Metiendo elementos a pilaTipoCrudo1 1.1 2.2 3.3 4.4 5.5 6.6 ExcepcionPilaLlena: La Pila esta llena, no se puede meter 6.6

Figura 18.12 | Programa de prueba de tipos crudos. (Parte 2 de 3).

www.elsolucionario.net

781

782

Capítulo 18

at at at at

Genéricos

Pila.push(Pila.java:30) PruebaTipoCrudo.probarPush(PruebaTipoCrudo.java:43) PruebaTipoCrudo.probarPilas(PruebaTipoCrudo.java:22) PruebaTipoCrudo.main(PruebaTipoCrudo.java:79)

Sacando elementos de pilaTipoCrudo1 5.5 4.4 3.3 2.2 1.1 ExcepcionPilaVacia: La Pila esta vacia, no se puede sacar at Pila.pop(Pila.java:40) at PruebaTipoCrudo.probarPop(PruebaTipoCrudo.java:65) at PruebaTipoCrudo.probarPilas(PruebaTipoCrudo.java:23) at PruebaTipoCrudo.main(PruebaTipoCrudo.java:79) Metiendo elementos a pilaTipoCrudo2 1.1 2.2 3.3 4.4 5.5 6.6 ExcepcionPilaLlena: La Pila esta llena, no se puede meter 6.6 at Pila.push(Pila.java:30) at PruebaTipoCrudo.probarPush(PruebaTipoCrudo.java:43) at PruebaTipoCrudo.probarPilas(PruebaTipoCrudo.java:24) at PruebaTipoCrudo.main(PruebaTipoCrudo.java:79) Sacando elementos de pilaTipoCrudo2 5.5 4.4 3.3 2.2 1.1 ExcepcionPilaVacia: La Pila esta vacia, no se puede sacar at Pila.pop(Pila.java:40) at PruebaTipoCrudo.probarPop(PruebaTipoCrudo.java:65) at PruebaTipoCrudo.probarPilas(PruebaTipoCrudo.java:25) at PruebaTipoCrudo.main(PruebaTipoCrudo.java:79) Metiendo elementos a pilaInteger 1 2 3 4 5 6 7 8 9 10 11 ExcepcionPilaLlena: La Pila esta llena, no se puede meter 11 at Pila.push(Pila.java:30) at PruebaTipoCrudo.probarPush(PruebaTipoCrudo.java:43) at PruebaTipoCrudo.probarPilas(PruebaTipoCrudo.java:26) at PruebaTipoCrudo.main(PruebaTipoCrudo.java:79) Sacando elementos de pilaInteger 10 9 8 7 6 5 4 3 2 1 ExcepcionPilaVacia: La Pila esta vacia, no se puede sacar at Pila.pop(Pila.java:40) at PruebaTipoCrudo.probarPop(PruebaTipoCrudo.java:65) at PruebaTipoCrudo.probarPilas(PruebaTipoCrudo.java:27) at PruebaTipoCrudo.main(PruebaTipoCrudo.java:79)

Figura 18.12 | Programa de prueba de tipos crudos. (Parte 3 de 3). La figura 18.13 muestra los mensajes de advertencia generados por el compilador (al compilar con la opción cuando se compila el archivo PruebaTipoCrudo.java (figura 18.12). La primera advertencia se genera para la línea 20, en la cual se asigna un tipo crudo Pila a una variable Pila< Integer >; el compilador no puede asegurar que todos los objetos en la Pila sean objetos Integer. La segunda advertencia se genera para la línea 22. Debido a que el segundo argumento del método es una variable Pila de tipo crudo, el compilador determina el argumento de tipo para el método probarPush del arreglo Double que se pasa como tercer argumento. En este caso, Double es el argumento de tipo, por lo que el compilador espera que se pase una Pila< Double > como segundo argumento. La advertencia ocurre debido a que el compilador no puede asegurar que una Pila de tipo crudo contenga sólo objetos Double. La advertencia en la línea 24 ocurre por la misma razón, aun cuando la Pila actual a la que pilaTipoCrudo2 hace referencia es una Pila< Double >. El compilador no puede garantizar que la variable siempre hará referencia al mismo objeto Pila, por lo que debe utilizar el tipo declarado de la variable para realizar toda la comprobación de tipos. En las líneas 23 y 25 se –Xlint:unchecked)

www.elsolucionario.net

18.8

Comodines en métodos que aceptan parámetros de tipo

783

PruebaTipoCrudo.java:20: warning: unchecked assignment found : Pila required: Pila Pila< Integer > pilaInteger = new Pila( 10 ); ^ PruebaTipoCrudo.java:22: warning: [unchecked] unchecked method invocation: probarPush(java.lang.String,Pila,T[]) in PruebaTipoCrudo is applied to (java.lang.String,Pila,java.lang.Double[]) probarPush( “pilaTipoCrudo1”, pilaTipoCrudo1, elementosDouble ); ^ PruebaTipoCrudo.java:23: warning: [unchecked] unchecked method invocation: probarPop(java.lang.String,Pila) in PruebaTipoCrudo is applied to ( java.lang.String,Pila) probarPop( “pilaTipoCrudo1”, pilaTipoCrudo1 ); ^ PruebaTipoCrudo.java:24: warning: [unchecked] unchecked method invocation: probarPush(java.lang.String,Pila,T[]) in PruebaTipoCrudo is applied to (java.lang.String,Pila,java.lang.Double[]) probarPush( “pilaTipoCrudo2”, pilaTipoCrudo2, elementosDouble ); ^ PruebaTipoCrudo.java:25: warning: [unchecked] unchecked method invocation: probarPop(java.lang.String,Pila) in PruebaTipoCrudo is applied to (java.lang.String,Pila) probarPop( “pilaTipoCrudo2”, pilaTipoCrudo2 ); ^ 5 warnings

Figura 18.13 | Mensajes de advertencia del compilador. generan advertencias debido a que el método probarPop espera como argumento una Pila para la cual se haya especificado un argumento de tipo. Sin embargo, en cada llamada a probarPop, pasamos una variable Pila de tipo crudo. Por ende, el compilador indica una advertencia, debido a que no puede comprobar los tipos utilizados en el cuerpo del método.

18.8 Comodines en métodos que aceptan parámetros de tipo En esta sección presentaremos un poderoso concepto de los genéricos, conocido como los comodines. Para este fin, también introduciremos una nueva estructura de datos del paquete java.util. El capítulo 19, Colecciones, habla sobre el Marco de trabajo Collections de Java, el cual proporciona muchas estructuras de datos genéricas y algoritmos que manipulan a los elementos de estas estructuras de datos. Tal vez la más simple de estas estructuras de datos sea la clase ArrayList: una estructura de datos tipo arreglo, que puede cambiar su tamaño en forma dinámica. Como parte de esta discusión, aprenderá a crear un objeto ArrayList, a añadirle elementos y a recorrer esos elementos mediante el uso de una instrucción for mejorada. Antes de presentar los comodines, analicemos un ejemplo que nos ayude a motivar su uso. Suponga que desea implementar un método genérico llamado suma, que obtenga el total de números en una colección, como en un objeto ArrayList. Para empezar, podría insertar los números en la colección. Como sabe, las clases genéricas sólo se pueden utilizar con tipos de clases o de interfaces. Por lo tanto, se realizaría una conversión autoboxing de los números a objetos de las clases de envoltura de tipos. Por ejemplo, cualquier valor int se convertiría mediante autoboxing en un objeto Integer, y cualquier valor double se convertiría mediante autoboxing en un objeto Double. Nos gustaría poder obtener el total de todos los números en el objeto ArrayList, sin importar su tipo. Por esta razón, declaramos el objeto ArrayList con el argumento de tipo Number, el cual es la superclase tanto de Integer como de Double. Además, el método suma recibirá un parámetro de tipo ArrayList< Number > y obtendrá el total de sus elementos. En la figura 18.14 se demuestra cómo obtener el total de los elementos de un objeto ArrayList de objetos Number. En la línea 11 se declara e inicializa un arreglo de objetos Number. Como los inicializadores son valores primitivos, Java realiza conversiones autoboxing en cada valor primitivo, para convertirlo en un objeto de su correspondiente tipo de envoltura. Los valores int 1 y 3 se convierten mediante autoboxing en objetos Integer,

www.elsolucionario.net

784

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

Capítulo 18

Genéricos

// Fig. 18.14: TotalNumeros.java // Suma de los elementos de un objeto ArrayList. import java.util.ArrayList; public class TotalNumeros { public static void main( String args[] ) { // crea, inicializa y muestra en pantalla el objeto ArrayList de objetos Number // que contiene objetos Integer y Double, después muestra el total de los elementos Number[] numeros = { 1, 2.4, 3, 4.1 }; // objetos Integer y Double ArrayList< Number > listaNumeros = new ArrayList< Number >(); for ( Number elemento : numeros ) listaNumeros.add( elemento ); // coloca cada número en listaNumeros System.out.printf( "listaNumeros contiene: %s\n", listaNumeros ); System.out.printf( "Total de los elementos en listaNumeros: %.1f\n", sumar( listaNumeros ) ); } // fin de main // calcula el total de los elementos de ArrayList public static double sumar( ArrayList< Number > lista ) { double total = 0; // inicializa el total // calcula la suma for ( Number elemento : lista ) total += elemento.doubleValue(); return total; } // fin del método sumar } // fin de la clase TotalNumeros

listaNumeros contiene: [1, 2.4, 3, 4.1] Total de los elementos en listaNumeros: 10.5

Figura 18.14 | Total de los números en ArrayList<

Number >.

y los valores double 2.4 y 4.1 se convierten mediante autoboxing en objetos Double. En la línea 12 se declara y crea un objeto ArrayList que almacena objetos Number, y se asigna a la variable listaNumeros. Observe que no tenemos que especificar el tamaño del objeto ArrayList, ya que crecerá de manera automática, a medida que insertemos objetos. En las líneas 14 y 15 se recorre el arreglo numeros y se coloca cada uno de sus elementos en listaNumeros. El método add de la clase ArrayList anexa un elemento al final de la colección. En la línea 17 se imprime en pantalla el contenido del objeto ArrayList como un objeto String. Esta instrucción invoca en forma implícita al método toString de ArrayList, el cual devuelve una cadena de la forma “[ elementos ]”, en la cual elementos es una lista separada por comas de las representaciones de cadena de los elementos. En las líneas 18 y 19 se muestra la suma de los elementos que se devuelve mediante la llamada al método suma en la línea 19. El método sumar (líneas 23 a 32) recibe un objeto ArrayList de objetos Number y calcula el total de los objetos Number en la colección. El método utiliza valores double para realizar los cálculos y devuelve el resultado como un double. En la línea 25 se declara la variable local total y se inicializa en 0. En las líneas 28 y 29 se utiliza la instrucción for mejorada, la cual está diseñada para trabajar con arreglos y con las colecciones del Marco de trabajo Collections, para obtener el total de los elementos del objeto ArrayList. La instrucción for asigna cada objeto Number en el objeto ArrayList a la variable elemento, y después utiliza el método doubleValue de la clase Number para obtener el valor primitivo subyacente del objeto Number como un valor double. El resultado se suma a total. Cuando el ciclo termina, el método devuelve el total.

www.elsolucionario.net

18.8

Comodines en métodos que aceptan parámetros de tipo

785

Cómo implementar el método suma con un argumento de tipo comodín en su parámetro Recuerde que el propósito del método suma en la figura 18.14 era obtener el total de cualquier tipo de objetos Number almacenados en un objeto ArrayList. Creamos un objeto ArrayList de objetos Number que contenía objetos Integer y Double. Los resultados de la figura 18.14 demuestran que el método sumar trabajó correctamente. Dado que el método sumar puede obtener el total de los elementos de un objeto ArrayList de objetos Number, podríamos esperar que el método también funcionara para objetos ArrayList que contengan elementos de sólo un tipo numérico, como ArrayList< Integer >. Así, modificamos la clase TotalNumeros para crear un objeto ArrayList de objetos Integer y pasarlo al método sumar. Al compilar el programa, el compilador genera el siguiente mensaje de error: sumar(java.util.ArrayList) in TotalNumerosErrores cannot be applied to (java.util.ArrayList)

Aunque Number es la superclase de Integer, el compilador no considera que el tipo parametrizado ArrayList < Number > sea un supertipo de ArrayList< Integer >. Si lo fuera, entonces toda operación que pudiéramos realizar en ArrayList< Number > funcionaría también en un ArrayList< Integer >. Considere el hecho de que puede sumar un objeto Double a un ArrayList< Number >, debido a que un Double es un Number, pero no se puede sumar un objeto Double a un ArrayList< Integer >, ya que un Double no es un Integer. Por ende, no es válida la relación de los subtipos. ¿Cómo creamos una versión más flexible del método sumar que pueda obtener el total de los elementos de cualquier objeto ArrayList que contenga elementos de cualquier subclase de Number? Aquí es donde son importantes los argumentos tipo comodín. Los comodines nos permiten especificar parámetros de métodos, valores de retorno, variables o campos, etc., que actúan como supertipos de los tipos parametrizados. En la figura 18.15, el parámetro del método suma se declara en la línea 50 con el tipo: ArrayList< ? extends Number >

Un argumento tipo comodín se denota mediante un signo de interrogación (?), que por sí solo representa un “tipo desconocido”. En este caso, el comodín extiende a la clase Number, lo cual significa que el comodín tiene un límite superior de Number. Por ende, el argumento de tipo desconocido debe ser Number o una subclase de Number. Con el tipo del parámetro que se muestra aquí, el método sumar puede recibir un argumento ArrayList que contenga cualquier tipo de Number, como ArrayList< Integer > (línea 20), ArrayList< Double > (línea 33) o ArrayList< Number > (línea 46). En las líneas 11 a 20 se crea e inicializa un objeto ArrayList< Integer > llamado listaEnteros, se imprimen en pantalla sus elementos y se obtiene el total de los mismos mediante una llamada al método sumar (línea 20). En las líneas 24 a 33 se realizan las mismas operaciones para un objeto ArrayList< Double > llamado listaDouble. En las líneas 37 a 46 se realizan las mismas operaciones para un objeto ArrayList< Number > llamado listaNumeros, el cual contiene objetos Integer y Double. En el método sumar (líneas 50 a 59), aunque los tipos de los elementos del argumento ArrayList no son directamente conocidos para el método, se sabe que por lo menos son de tipo Number, ya que el comodín se especificó con el límite superior Number. Por esta razón se permite la línea 56, ya que todos los objetos Number tienen un método doubleValue. Aunque los comodines proporcionan una flexibilidad al pasar tipos parametrizados a un método, también tienen ciertas desventajas. Como el comodín (?) en el encabezado del método (línea 50) no especifica el nombre de un parámetro de tipo, no se puede utilizar como nombre de tipo en el cuerpo del método (es decir, no podemos reemplazar Numero con ? en la línea 55). Sin embargo, podríamos declarar el método sumar de la siguiente manera: public static double sumar( ArrayList< T > lista )

lo cual permite al método recibir un objeto ArrayList que contenga elementos de cualquier subclase de Number. Después, podríamos usar el parámetro de tipo T en el cuerpo del método. Si el comodín se especifica sin un límite superior, entonces sólo se pueden invocar los métodos del tipo Object en valores del tipo del comodín. Además, los métodos que utilizan comodines en los argumentos de tipo de sus parámetros no pueden utilizarse para agregar elementos a una colección a la que el parámetro hace referencia.

www.elsolucionario.net

786

Capítulo 18

Genéricos

Error común de programación 18.4 Utilizar un comodín en la sección de parámetros de tipo de un método, o utilizar un comodín como un tipo explícito de una variable en el cuerpo del método, es un error de sintaxis.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53

// Fig. 18.15: PruebaComodin.java // Programa de prueba de comodines. import java.util.ArrayList; public class PruebaComodin { public static void main( String args[] ) { // crea, inicializa e imprime en pantalla un objeto ArrayList de objetos Integer, // y después muestra el total de los elementos Integer[] enteros = { 1, 2, 3, 4, 5 }; ArrayList< Integer > listaEnteros = new ArrayList< Integer >(); // inserta elementos en listaEnteros for ( Integer elemento : enteros ) listaEnteros.add( elemento ); System.out.printf( "listaEnteros contiene: %s\n", listaEnteros ); System.out.printf( "Total de los elementos en listaEnteros: %.0f\n\n", sumar( listaEnteros ) ); // crea, inicializa e imprime en pantalla un objeto ArrayList de objetos Doubles, // y después muestra el total de los elementos Double[] valoresDouble = { 1.1, 3.3, 5.5 }; ArrayList< Double > listaDouble = new ArrayList< Double >(); // inserta elementos en listaDouble for ( Double elemento : valoresDouble ) listaDouble.add( elemento ); System.out.printf( "listaDouble contiene: %s\n", listaDouble ); System.out.printf( "Total de los elementos en listaDouble: %.1f\n\n", sumar( listaDouble ) ); // crea, inicializa e imprime en pantalla un objeto ArrayList de objetos Number // que contiene objetos Integer y Double, después muestra el total de los elementos Number[] numeros = { 1, 2.4, 3, 4.1 }; // objetos Integer y Double ArrayList< Number > listaNumeros = new ArrayList< Number >(); // inserta elementos en listaNumeros for ( Number elemento : numeros ) listaNumeros.add( elemento ); System.out.printf( "listaNumeros contiene: %s\n", listaNumeros ); System.out.printf( "Total de los elementos en listaNumeros: %.1f\n", sumar( listaNumeros ) ); } // fin de main // calcula el total de los elementos de la pila public static double sumar( ArrayList< ? extends Number > lista ) { double total = 0; // inicializa el total

Figura 18.15 | Programa de prueba de comodines. (Parte 1 de 2).

www.elsolucionario.net

18.11

54 55 56 57 58 59 60

Recursos en Internet y Web

787

// calcula la suma for ( Number elemento : lista ) total += elemento.doubleValue(); return total; } // fin del método sumar } // fin de la clase PruebaComodin

listaEnteros contiene: [1, 2, 3, 4, 5] Total de los elementos en listaEnteros: 15 listaDouble contiene: [1.1, 3.3, 5.5] Total de los elementos en listaDouble: 9.9 listaNumeros contiene: [1, 2.4, 3, 4.1] Total de los elementos en listaNumeros: 10.5

Figura 18.15 | Programa de prueba de comodines. (Parte 2 de 2).

18.9 Genéricos y herencia: observaciones Los genéricos pueden utilizarse con la herencia de varias formas: • Una clase genérica puede derivarse de una clase no genérica. Por ejemplo, la clase Object es una superclase directa o indirecta de toda clase genérica. • Una clase genérica puede derivarse de otra clase genérica. Por ejemplo, la clase genérica Stack (en el paquete java.util) es una subclase de la clase genérica Vector (en el paquete java.util). En el capítulo 19, Colecciones, hablaremos sobre estas clases. • Una clase no genérica puede derivarse de una clase genérica. Por ejemplo, la clase no genérica Properties (en el paquete java.util) es una subclase de la clase genérica Hashtable (en el paquete java. util). También veremos estas clases en el capítulo 19. • Por último, un método genérico en una subclase puede sobrescribir a un método genérico en una superclase, si ambos métodos tienen las mismas firmas.

18.10 Conclusión En este capítulo se presentaron los genéricos. Usted aprendió a declarar métodos genéricos y clases genéricas. Aprendió cómo se logra la compatibilidad inversa mediante tipos crudos. También aprendió a utilizar comodines en un método genérico o en una clase genérica. En el siguiente capítulo demostraremos las interfaces, clases y algoritmos del marco de trabajo de colecciones de Java. Como veremos, todas las colecciones que se presentarán utilizan las capacidades genéricas que vimos en este capítulo.

18.11 Recursos en Internet y Web www.jcp.org/aboutJava/communityprocess/review/jsr014/

Página de descarga del Proceso comunitario de Java, para el documento de la especificación de genéricos Adding Generics to the Java Programming Language: Public Draft Specification, Version 2.0. www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html

Una colección de preguntas frecuentes acerca de los genéricos en Java. java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf

El tutorial Generics in the Java Programming Language por Gilad Bracha (el líder de la especificación para JSR-14 y revisor de este libro) introduce los conceptos de los genéricos, con fragmentos de código de ejemplo. today.java.net/pub/a/today/2003/12/02/explorations.html today.java.net/pub/a/today/2004/01/15/wildcards.html

Los artículos Explorations: Generics, Erasure, and Bridging y Explorations: Wildcards in the Generics Specification, cada uno por William Grosso, presentan las generalidades acerca de las características de los genéricos, y cómo utilizar los comodines.

www.elsolucionario.net

788

Capítulo 18

Genéricos

Resumen Sección 18.1 Introducción • Los métodos genéricos permiten a los programadores especificar, con la declaración de un solo método, un conjunto de métodos relacionados. • Las clases genéricas permiten a los programadores especificar, con la declaración de una sola clase, un conjunto de tipos relacionados. • Los métodos genéricos y las clases genéricas se encuentran entre las herramientas más poderosas de Java para la reutilización de software con seguridad en los tipos.

Sección 18.2 Motivación para los métodos genéricos • Los métodos sobrecargados se utilizan a menudo para realizar operaciones similares en distintos tipos de datos. • Cuando el compilador encuentra la llamada a un método, trata de localizar la declaración de un método que tenga el mismo nombre del método y los mismos parámetros que coincidan con los tipos de los argumentos en la llamada al método.

Sección 18.3 Métodos genéricos: implementación y traducción en tiempo de compilación • Si las operaciones realizadas por varios métodos sobrecargados son idénticas para cada tipo de argumento, los métodos sobrecargados se pueden codificar en forma más compacta y conveniente, mediante el uso de un método genérico. Usted puede escribir la declaración de un solo método genérico, el cual se puede llamar con argumentos de distintos tipos de datos. Con base en los tipos de los argumentos que se pasan al método genérico, el compilador maneja la llamada a cada método en forma apropiada. • Todas las declaraciones de métodos genéricos tienen una sección de parámetros de tipo, delimitada por los signos < y >, que antecede al tipo de valor de retorno del método. • Cada sección de parámetros de tipo contiene uno o más parámetros de tipo (también llamados parámetros de tipo formal), separados por comas. • Un parámetro de tipo es un identificador que especifica el nombre de un tipo genérico. Los parámetros de tipo pueden utilizarse como el tipo de valor de retorno, los tipos de los parámetros y los tipos de las variables locales en la declaración de un método genérico, y actúan como receptáculos para los tipos de los argumentos que se pasan al método genérico, los cuales se conocen como argumentos de tipo actuales. Los parámetros de tipo sólo pueden representar tipos de referencias. • Los nombres utilizados para los parámetros de tipo en la declaración de un método deben coincidir con los que se declaran en la sección de parámetros de tipo. El nombre de un parámetro de tipo se puede declarar sólo una vez en la sección de parámetros de tipo, pero puede aparecer más de una vez en la lista de parámetros del método. Los nombres de los parámetros de tipo no necesitan ser únicos entre distintos métodos genéricos. • Cuando el compilador encuentra la llamada a un método, determina los tipos de los argumentos y trata de localizar un método con el mismo nombre y parámetros que coincidan con los tipos de los argumentos. Si no hay un método así, el compilador determina si hay una coincidencia inexacta, pero aplicable. • El operador relacional > no se puede utilizar con tipos de referencias. Sin embargo, es posible comparar dos objetos de la misma clase, si esa clase implementa a la interfaz genérica Comparable (paquete java.lang). • Los objetos Comparable tienen un método compareTo que debe devolver 0 si los objetos son iguales, -1 si el primer objeto es menor que el segundo, o 1 si el primer objeto es mayor que el segundo. • Todas las clases de envoltura de tipos para los tipos primitivos implementan a Comparable. • Un beneficio de implementar la interfaz Comparable es que los objetos Comparable pueden utilizarse con los métodos de ordenamiento y búsqueda de la clase Collections (paquete java.util). • Cuando el compilador traduce un método genérico en códigos de byte de Java, elimina la sección de parámetros de tipo y reemplaza los parámetros de tipo con tipos actuales. A este proceso se le conoce como borrado. De manera predeterminada, cada parámetro de tipo se reemplaza con su límite superior. De manera predeterminada, el límite superior es de tipo Object, a menos que se especifique otra cosa en la sección de parámetros de tipo.

Sección 18.4 Cuestiones adicionales sobre la traducción en tiempo de compilación: métodos que utilizan un parámetro de tipo como tipo de valor de retorno • Cuando el compilador realiza el borrado en un método que devuelva una variable de tipo, también inserta operaciones de conversión explícitas en frente de cada llamada a un método, para asegurar que el valor devuelto sea del tipo que espera el método que hizo la llamada.

www.elsolucionario.net

Resumen

789

Sección 18.5 Sobrecarga de métodos genéricos • Un método genérico puede sobrecargarse. Una clase puede proporcionar dos o más métodos genéricos que especifiquen el mismo nombre del método, pero distintos parámetros para el mismo. Un método genérico también puede sobrecargarse mediante métodos no genéricos que tengan el mismo nombre y el mismo número de parámetros. Cuando el compilador encuentra la llamada a un método, busca la declaración del método que coincida en forma más precisa con el nombre del método y los tipos de los argumentos especificados en la llamada. • Cuando el compilador encuentra la llamada a un método, realiza un proceso de asociación para determinar cuál método debe llamar. El compilador trata de buscar y utilizar una coincidencia precisa, en la cual los nombres del método y los tipos de los argumentos de la llamada al método coincidan con los de una declaración específica del método. Si esto falla, el compilador determina si hay un método genérico disponible que proporcione una coincidencia precisa del nombre del método y los tipos de los argumentos y, de ser así, utiliza ese método genérico.

Sección 18.6 Clases genéricas • Las clases genéricas proporcionan los medios para describir una clase en forma independiente del tipo. Así, podemos instanciar objetos específicos del tipo de la clase genérica. • La declaración de una clase genérica es similar a la declaración de una clase no genérica, excepto que el nombre de la clase va seguido de una sección de parámetros de tipo. Al igual que con los métodos genéricos, la sección de parámetros de tipo de una clase genérica puede tener uno o más parámetros de tipo, separados por comas. • Cuando se compila una clase genérica, el compilador realiza el borrado en los parámetros de tipo de la clase y los reemplaza con sus límites superiores. • Los parámetros de tipo no se pueden utilizar en las declaraciones static de una clase. • Al instanciar un objeto de una clase genérica, los tipos especificados entre los signos < y > después del nombre de la clase se conocen como argumentos de tipo. El compilador los utiliza para reemplazar los parámetros de tipo, de manera que pueda realizar la comprobación de tipos e insertar operaciones de conversión, según sea necesario.

Sección 18.7 Tipos crudos (raw) • Es posible instanciar una clase genérica sin especificar un argumento de tipo. En este caso, se dice que el nuevo objeto de la clase tiene un tipo crudo, lo cual significa que el compilador utiliza de manera implícita el tipo Object (o el límite superior del parámetro de tipo) en la clase genérica para cada argumento de tipo.

Sección 18.8 Comodines en métodos que aceptan parámetros de tipo • El Marco de trabajo Collections de Java proporciona muchas estructuras de datos genéricas y algoritmos para manipular los elementos de esas estructuras de datos. Tal vez la más simple de las estructuras de datos sea la clase ArrayList; una estructura de datos tipo arreglo, que puede cambiar su tamaño en forma dinámica. • La clase Number es la superclase de Integer y de Double. • El método add de la clase ArrayList anexa un elemento al final de la colección. • El método toString de la clase ArrayList devuelve una cadena de la forma “[ elementos ]”, en la cual elementos es una lista separada por comas de las representaciones de cadena de los elementos. • El método doubleValue de la clase Number obtiene el valor primitivo subyacente de Number como un valor double. • Los argumentos tipo comodín nos permiten especificar parámetros de métodos, valores de retorno, variables, etcétera, que actúen como supertipos de los tipos parametrizados. Un argumento de tipo comodín se denota mediante el signo de interrogación (?), el cual representa un “tipo desconocido”. Un comodín también puede tener un límite superior. • Debido a que un comodín (?) no es el nombre de un parámetro de tipo, no se puede utilizar como nombre de tipo en el cuerpo de un método. • Si se especifica un comodín sin un límite superior, entonces sólo pueden invocarse los métodos de tipo Object en valores del tipo comodín. • Los métodos que utilizan comodines como argumentos de tipo no pueden usarse para agregar elementos a una colección a la que hace referencia el parámetro.

Sección 18.9 Genéricos y herencia: observaciones • Una clase genérica puede derivarse de una clase no genérica. Por ejemplo, Object es una superclase directa o indirecta de toda clase genérica. • Una clase genérica puede derivarse de otra clase genérica. • Una clase no genérica puede derivarse de una clase genérica.

www.elsolucionario.net

790

Capítulo 18

Genéricos

• Un método genérico en una subclase puede sobrescribir a un método genérico en una superclase, si ambos métodos tienen las mismas firmas.

Terminología Integer, clase interfaz genérica límite superior de un comodín límite superior de un parámetro de tipo límite superior predeterminado de un parámetro de tipo método genérico Number, clase parámetro de tipo parámetro de tipo formal sección de parámetros de tipo signos < y > sobrecargar un método genérico tipo crudo (raw) tipo parametrizado toString, método de ArrayList variable de tipo

? (argumento de tipo comodín) add, método de ArrayList

alcance de un parámetro de tipo argumento de tipo argumentos de tipo actuales ArrayList, clase borrado clase genérica clase parametrizada comodín como argumento de tipo comodín sin un límite superior comodín (?) Comparable, interfaz compareTo, método de Comparable Double, clase doubleValue, método de Number genéricos

Ejercicios de autoevaluación 18.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 genérico no puede tener el mismo nombre que un método no genérico. b) Todas las declaraciones de métodos genéricos tienen una sección de parámetros de tipo, la cual va justo antes del nombre del método. c) Un método genérico puede sobrecargarse mediante otro método genérico con el mismo nombre, pero con distintos parámetros. d) Un parámetro de tipo puede declararse sólo una vez en la sección de parámetros de tipo, pero puede aparecer más de una vez en la lista de parámetros del método. e) Los nombres de los parámetros de tipo entre los distintos métodos genéricos deben ser únicos. f ) El alcance del parámetro de tipo de una clase genérica es toda la clase, excepto sus miembros static. 18.2

Complete los siguientes enunciados: a) Los _________________ y las _________________ le permiten especificar, con la declaración de un solo método, un conjunto de métodos relacionados, o con la declaración de una sola clase, un conjunto de tipos relacionados, respectivamente. b) Una sección de parámetros de tipo se delimita mediante _________________. c) Los _________________ de un método genérico se pueden usar para especificar los tipos de los argumentos del método, para especificar el tipo de valor de retorno y para declarar variables dentro del método. d) La instrucción "Pila pilaObjetos = new Pila();" indica que pilaObjetos almacena ___________. e) En la declaración de una clase genérica, el nombre de la clase va seguido por un(a) _________________. f ) La sintaxis _________________ especifica que el límite superior de un comodín es de tipo E.

Respuestas a los ejercicios de autoevaluación 18.1 a) Falso. Los métodos genéricos y los no genéricos pueden tener el mismo nombre. Un método genérico puede sobrecargar a otro método genérico con el mismo nombre, pero con distintos parámetros. Un método genérico también puede sobrecargarse si se proporcionan métodos no genéricos con el mismo nombre del método y el mismo número de argumentos. b) Falso. Todas las declaraciones de métodos tienen una sección de parámetros de tipo que va justo antes

www.elsolucionario.net

Ejercicios

791

del tipo de valor de retorno del método. c) Verdadero. d) Verdadero. e) Falso. Los nombres de los parámetros de tipo entre los distintos métodos genéricos no tienen que ser únicos. f ) Verdadero. 18.2 a) Métodos genéricos, clases genéricas. b) los signos < y >. c) parámetros de tipo. d) un tipo crudo (raw). e) sección de parámetros de tipo. f ) ? extends E.

Ejercicios 18.3

Explique el uso de la siguiente notación en un programa en Java: public class Array< T > { }

18.4 Escriba un método genérico llamado ordenamientoSeleccion con base en el programa de ordenamiento de las figuras 16.6 y 16.7. Escriba un programa de prueba que introduzca, ordene e imprima en pantalla un arreglo Integer y un arreglo Float. [Sugerencia: use > en la sección de parámetros de tipo para el método ordenamientoSeleccion, para que pueda utilizar el método compareTo para comparar los objetos de dos tipos genéricos T]. 18.5 Sobrecargue el método genérico imprimirArreglo de la figura 18.3 para que reciba dos argumentos enteros adicionales, subindiceInferior y subindiceSuperior. Una llamada a este método debe imprimir sólo la parte designada del arreglo. Valide subindiceInferior y subindiceSuperior. Si cualquiera de los dos está fuera de rango, o si subindiceSuperior es menor o igual que subindiceInferior, el método imprimirArreglo sobrecargado debe lanzar una excepción InvalidSubscriptException; en caso contrario, imprimirArreglo debe devolver el número de elementos impresos. Después modifique main para ejecutar ambas versiones de imprimirArreglo en los arreglos arregloInteger, arregloDouble y arregloCharacter. Pruebe todas las capacidades de ambas versiones de imprimirArreglo. 18.6 Sobrecargue el método genérico imprimirArreglo de la figura 18.3 con una versión no genérica que imprima en forma específica un arreglo de cadenas en un formato tabular impecable, como se muestra en los resultados de ejemplo a continuación: El arreglo arregloCadena contiene: uno dos tres cuatro cinco seis siete ocho

Escriba una versión genérica simple del método esIgualA que compare sus dos argumentos con el método equals y devuelva true si son iguales, y false en caso contrario. Use este método genérico en un programa que llame a esIgualA con una variedad de tipos integrados, como Object o Integer. ¿Qué resultado obtiene al tratar de ejecutar 18.7

este programa? 18.8 Escriba una clase genérica llamada Par, que tenga dos parámetros de tipo: F y S, cada uno de los cuales representa el tipo del primer y segundo elementos del par, respectivamente. Agregue métodos obtener y establecer para los elementos primero y segundo del par. [Sugerencia: el encabezado de la clase debe ser public class Par< F, S >]. 18.9 Convierta las clases NodoArbol y Arbol de la figura 17.17 en clases genéricas. Para insertar un objeto en un Arbol, el objeto debe compararse con los objetos en los objetos NodoArbol existentes. Por esta razón, las clases NodoArbol y Arbol deben especificar a Comparable< E > como el límite superior del parámetro de tipo de cada clase. Después de modificar las clases NodoArbol y Arbol, escriba una aplicación de prueba para crear tres objetos Arbol: uno que almacene objetos Integer, uno que almacene objetos Double y uno que almacene objetos String. Inserte 10 valores en cada árbol. Después imprima en pantalla los recorridos preorden, inorden y postorden para cada Arbol. 18.10 Modifique su programa de prueba del ejercicio 18.9 para utilizar un método genérico llamado probarArbol, para probar los tres objetos Arbol. El método debe llamarse tres veces, una para cada objeto Arbol. 18.11 ¿Cómo pueden sobrecargarse los métodos genéricos? 18.12 El compilador realiza un proceso de asociación para determinar cuál método debe llamar al invocar a un método. ¿Bajo qué circunstancias un intento por realizar una asociación produce un error en tiempo de compilación? 18.13 Explique por qué un programa en Java podría utilizar la instrucción ArrayList< Empleado > listaTrabajadores = new ArrayList< Empleado >();

www.elsolucionario.net

19 Colecciones Creo que ésta es la colección más extraordinaria de talento, de conocimiento humano, que se haya reunido en la Casa Blanca; con la posible excepción de cuando Thomas Jefferson comió solo.

OBJETIVOS

— John F. Kennedy

En este capítulo aprenderá a: Q

Comprender lo que son las colecciones.

Q

Utilizar la clase Arrays para manipulaciones comunes de arreglos.

Q

Utilizar las implementaciones del marco de trabajo de colecciones (estructura de datos preempaquetada).

Q

Utilizar los algoritmos del marco de trabajo de colecciones para manipular varias colecciones (como search, sort y fill).

Q

Utilizar las interfaces del marco de trabajo de colecciones para programar mediante el polimorfismo.

Q

Utilizar iteradores para “recorrer” los elementos de una colección.

Q

Utilizar las tablas hash persistentes que se manipulan con objetos de la clase Properties.

Q

Comprender las envolturas de sincronización y las envolturas modificables.

¡Las figuras que puede alojar un contenedor brillante! — Theodore Roethke

Un viaje a través de todo el universo en un mapa. — Miguel de Cervantes

La sabiduría se adquiere, no por la edad, sino por la capacidad. — Tirus Maccius Plautus

Es un acertijo envuelto en un misterio, dentro de un enigma. — Winston Churchill

www.elsolucionario.net

Pla n g e ne r a l

19.1 Introducción

19.1 19.2 19.3 19.4 19.5

19.6

19.7 19.8 19.9 19.10 19.11 19.12 19.13 19.14 19.15

793

Introducción Generalidades acerca de las colecciones La clase Arrays La interfaz Collection y la clase Collections Listas ArrayList e Iterator 19.5.1 19.5.2 LinkedList 19.5.3 Vector Algoritmos de las colecciones 19.6.1 El algoritmo sort 19.6.2 El algoritmo shuffle 19.6.3 Los algoritmos reverse, fill, copy, max y min 19.6.4 El algoritmo binarySearch 19.6.5 Los algoritmos addAll, frequency y disjoint La clase Stack del paquete java.util La clase PriorityQueue y la interfaz Queue Conjuntos Mapas La clase Properties Colecciones sincronizadas Colecciones no modificables Implementaciones abstractas Conclusión

Resumen | Terminología | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

19.1 Introducción En el capítulo 17 vimos cómo crear y manipular estructuras de datos. La discusión fue de “bajo nivel”, en cuanto a que creamos laboriosamente cada elemento de cada estructura de datos en forma dinámica, y modificamos las estructuras de datos manipulando directamente sus elementos y las referencias a ellos. En este capítulo veremos el marco de trabajo de colecciones de Java, el cual contiene estructuras de datos, interfaces y algoritmos preempaquetados para manipular esas estructuras de datos. Algunos ejemplos de colecciones son las tarjetas que usted posee en un juego de cartas, su música favorita almacenada en su computadora, los miembros de un equipo deportivo y los registros de bienes raíces en el registro de propiedades de su localidad (que asocia números de libro y página con los propietarios de los bienes inmuebles). En este capítulo también veremos cómo se utilizan los genéricos (vea el capítulo 18) en el marco de trabajo de colecciones de Java. Con las colecciones, los programadores utilizan las estructuras de datos existentes sin tener que preocuparse por la manera en que éstas se implementan. Éste es un maravilloso ejemplo de reutilización de código. Los programadores pueden codificar más rápido y esperar un excelente rendimiento, maximizando la velocidad de ejecución y minimizando el consumo de memoria. En este capítulo hablaremos sobre las interfaces del marco de trabajo de colecciones que describen las capacidades de cada tipo de colección, las clases de implementación, los algoritmos que procesan a las colecciones y los denominados iteradores, junto con la sintaxis de la instrucción for mejorada para “recorrer” las colecciones. En este capítulo presentamos una introducción al marco de trabajo de colecciones. Para obtener los detalles completos, visite la página Web java.sun.com/javase/6/docs/guide/ collections. El marco de trabajo de colecciones de Java proporciona componentes reutilizables, listos para utilizarse; usted no necesita escribir sus propias clases de colecciones, pero puede hacerlo si lo desea. Estas colecciones están estandarizadas, de manera que las aplicaciones puedan compartirlas fácilmente sin tener que preocuparse por los

www.elsolucionario.net

794

Capítulo 19

Colecciones

detalles relacionados con su implementación. El marco de trabajo de colecciones fomenta aún más la reutilización. A medida que se desarrollen estructuras de datos y algoritmos que se ajusten a este marco de trabajo, una extensa base de programadores estará ya familiarizada con las interfaces y algoritmos implementados por esas estructuras de datos.

19.2 Generalidades acerca de las colecciones Una colección es una estructura de datos (en realidad, un objeto) que puede guardar referencias a otros objetos. Por lo general, las colecciones contienen referencias a objetos, los cuales son todos del mismo tipo. Las interfaces del marco de trabajo de colecciones declaran las operaciones que se deben realizar en forma genérica en varios tipos de colecciones. La figura 19.1 enlista algunas de las interfaces del marco de trabajo de colecciones. Varias implementaciones de estas interfaces se proporcionan dentro del marco de trabajo. Los programadores también pueden proporcionar implementaciones específicas para sus propios requerimientos. Interfaz

Descripción

Collection

La interfaz raíz en la jerarquía de colecciones, a partir de la cual se derivan las interfaces Set, Queue y List.

Set

Una colección que no contiene duplicados.

List

Una colección ordenada que puede contener elementos duplicados.

Map

Asocia claves con valores y no puede contener claves duplicadas.

Queue

Por lo general, una colección del tipo primero en entrar, primero en salir, que modela a una línea de espera; pueden especificarse otros órdenes.

Figura 19.1 | Algunas interfaces del marco de trabajo de colecciones. El marco de trabajo de colecciones proporciona implementaciones de alto rendimiento y alta calidad de las estructuras de datos comunes, y permite la reutilización de software. Estas características minimizan la cantidad de código que necesitan escribir los programadores para crear y manipular colecciones. Las clases y las interfaces del marco de trabajo de colecciones son miembros del paquete java.util. En la siguiente sección, comenzaremos nuestra discusión mediante un análisis de las herramientas del marco de trabajo de colecciones para la manipulación de arreglos. En versiones anteriores de Java, las clases en el marco de trabajo de colecciones almacenaban y manipulaban referencias Object. Por ende, podíamos almacenar cualquier objeto en una colección. Un aspecto inconveniente de almacenar referencias Object se presenta al obtenerlas de una colección. Por lo general, un programa tiene la necesidad de procesar tipos específicos de objetos. Como resultado, las referencias Object que se obtienen de una colección comúnmente necesitan convertirse en un tipo apropiado, para permitir que el programa procese los objetos correctamente. En Java SE 5, el marco de trabajo de colecciones se mejoró con las herramientas de genéricos que presentamos en el capítulo 18. Esto significa que podemos especificar el tipo exacto que se almacenará en una colección. También recibimos los beneficios de la comprobación de tipos en tiempo de ejecución; el compilador asegura que se utilicen los tipos apropiados con la colección y, si no es así, emite mensajes de error en tiempo de compilación. Además, una vez que especifique el tipo almacenado en una colección, cualquier referencia que obtenga de la colección tendrá el tipo especificado. Esto elimina la necesidad de conversiones de tipo explícitas que pueden lanzar excepciones ClassCastException, si el objeto referenciado no es del tipo apropiado. Los programas que se implementaron con versiones anteriores de Java y que utilizan colecciones pueden compilarse de manera apropiada, ya que el compilador utiliza de manera automática los tipos crudos (raw) cuando encuentra colecciones para las cuales no se especificaron argumentos de tipo.

19.3 La clase Arrays

La clase Arrays proporciona métodos static para manipular arreglos. En el capítulo 7, nuestra discusión acerca de la manipulación de arreglos fue de nivel bajo, en el sentido en que escribimos el código en sí para ordenar y

www.elsolucionario.net

19.3

La clase Arrays

795

buscar en los arreglos. La clase Arrays proporciona métodos de alto nivel, como sort para ordenar un arreglo, binarySearch para buscar en un arreglo ordenado, equals para comparar arreglos y fill para colocar valores en un arreglo. Estos métodos se sobrecargan para los arreglos de tipo primitivo y los arreglos tipo Object. Además, los métodos sort y binarySearch están sobrecargados con versiones genéricas que permiten a los programadores ordenar y buscar en arreglos que contengan objetos de cualquier tipo. En la figura 19.2 se demuestra el uso de los métodos fill, sort, binarySearch y equals. El método main (líneas 65 a 85) crea un objeto UsoArrays e invoca a sus métodos. En la línea 17 se hace una llamada al método static fill de Arrays para llenar los 10 elementos del arreglo arregloIntLleno con 7s. Las versiones sobrecargadas de fill permiten al programador llenar un rango específico de elementos con el mismo valor. En la línea 18 se ordenan los elementos del arreglo arregloDouble. El método static sort de la clase Arrays ordena los elementos del arreglo en orden ascendente, de manera predeterminada. Más adelante en este capítulo veremos cómo ordenar elementos en forma descendente. Las versiones sobrecargadas de sort permiten al programador ordenar un rango específico de elementos. En las líneas 21 y 22 se copia el arreglo arregloInt en el arreglo copiaArregloInt. El primer argumento (arregloInt) que se pasa al método arraycopy de System es el arreglo a partir del cual se van a copiar los elementos. El segundo argumento (0) es el índice que especifica el punto de inicio en el rango de elementos que se van a copiar del arreglo. Este valor puede ser cualquier índice de arreglo válido. El tercer argumento (copiaArregloInt) especifica el arreglo de destino que almacenará la copia. El cuarto argumento (0) especifica el índice en el arreglo de destino en donde deberá guardarse el primer elemento copiado. El último argumento especifica el número de elementos a copiar del arreglo en el primer argumento. En este caso copiaremos todos los elementos en el arreglo. En la línea 50 se hace una llamada al método estático binarySearch de la clase Arrays para realizar una búsqueda binaria en arregloInt, utilizando valor como la clave. Si se encuentra valor, binarySearch devuelve el índice del elemento; en caso contrario, binarySearch devuelve un valor negativo. El valor negativo devuelto se basa en el punto de inserción de la clave de búsqueda: el índice en donde se insertaría la clave en el arreglo si se fuera a realizar una operación de inserción. Una vez que binarySearch determina el punto de inserción, cambia el signo de éste a negativo y le resta 1 para obtener el valor de retorno. Por ejemplo, en la figura 19.2, el punto de inserción para el valor 8763 es el elemento en el arreglo con el índice 6. El método binarySearch cambia el punto de inserción a –6, le resta 1 y devuelve el valor –7. Al restar 1 al punto de inserción se garantiza que el método binarySearch devuelva valores positivos (>= 0) sí, y sólo si se encuentra la clave. Este valor de retorno es útil para agregar elementos en un arreglo ordenado. En el capítulo 16, Búsqueda y ordenamiento, se habla sobre la búsqueda binaria con detalle.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

// Fig. 19.2: UsoArrays.java // Uso de arreglos en Java. import java.util.Arrays; public class UsoArrays { private int arregloInt[] = { 1, 2, 3, 4, 5, 6 }; private double arregloDouble[] = { 8.4, 9.3, 0.2, 7.9, 3.4 }; private int arregloIntLleno[], copiaArregloInt[]; // el constructor inicializa los arreglos public UsoArrays() { arregloIntLleno = new int[ 10 ]; // crea arreglo int con 10 elementos copiaArregloInt = new int[ arregloInt.length ]; Arrays.fill( arregloIntLleno, 7 ); // llena con 7s Arrays.sort( arregloDouble ); // ordena arregloDouble en forma ascendente

Figura 19.2 | Métodos de la clase Arrays. (Parte 1 de 3).

www.elsolucionario.net

796

20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78

Capítulo 19

Colecciones

// copia el arreglo arregloInt en el arreglo copiaArregloInt System.arraycopy( arregloInt, 0, copiaArregloInt, 0, arregloInt.length ); } // fin del constructor de UsoArrays // imprime los valores en cada arreglo public void imprimirArreglos() { System.out.print( "arregloDouble: " ); for ( double valorDouble : arregloDouble ) System.out.printf( "%.1f ", valorDouble ); System.out.print( "\narregloInt: " ); for ( int valorInt : arregloInt ) System.out.printf( "%d ", valorInt ); System.out.print( "\narregloIntLleno: " ); for ( int valorInt : arregloIntLleno ) System.out.printf( "%d ", valorInt ); System.out.print( "\ncopiaArregloInt: " ); for ( int valorInt : copiaArregloInt ) System.out.printf( "%d ", valorInt ); System.out.println( "\n" ); } // fin del método imprimirArreglos // busca un valor en el arreglo arregloInt public int buscarUnInt( int valor ) { return Arrays.binarySearch( arregloInt, valor ); } // fin del método buscarUnInt // compara el contenido del arreglo public void imprimirIgualdad() { boolean b = Arrays.equals( arregloInt, copiaArregloInt ); System.out.printf( "arregloInt %s copiaArregloInt\n", ( b ? "==" : "!=" ) ); b = Arrays.equals( arregloInt, arregloIntLleno ); System.out.printf( "arregloInt %s arregloIntLleno\n", ( b ? "==" : "!=" ) ); } // fin del método imprimirIgualdad public static void main( String args[] ) { UsoArrays usoArreglos = new UsoArrays(); usoArreglos.imprimirArreglos(); usoArreglos.imprimirIgualdad(); int ubicacion = usoArreglos.buscarUnInt( 5 ); if ( ubicacion >= 0 ) System.out.printf( "Se encontro el 5 en el elemento %d de arregloInt\n", ubicacion ); else System.out.println( "No se encontro el 5 en arregloInt" );

Figura 19.2 | Métodos de la clase Arrays. (Parte 2 de 3).

www.elsolucionario.net

19.4

79 80 81 82 83 84 85 86

La interfaz Collection y la clase Collections

797

ubicacion = usoArreglos.buscarUnInt( 8763 ); if ( ubicacion >= 0 ) System.out.printf( "Se encontro el 8763 en el elemento %d en arregloInt\n", ubicacion ); else System.out.println( "No se encontro el 8763 en arregloInt" ); } // fin de main } // fin de la clase UsoArrays

arregloDouble: 0.2 3.4 7.9 8.4 9.3 arregloInt: 1 2 3 4 5 6 arregloIntLleno: 7 7 7 7 7 7 7 7 7 7 copiaArregloInt: 1 2 3 4 5 6 arregloInt == copiaArregloInt arregloInt != arregloIntLleno Se encontro el 5 en el elemento 4 de arregloInt No se encontro el 8763 en arregloInt

Figura 19.2 | Métodos de la clase Arrays. (Parte 3 de 3).

Error común de programación 19.1 Pasar un arreglo desordenado al método binarySearch es un error lógico; el valor devuelto es indefinido.

En las líneas 56 y 60 se hace una llamada al método static equals de la clase Arrays para determinar si los elementos de dos arreglos son equivalentes. Si los arreglos contienen los mismos elementos en el mismo orden, el método devuelve true; en caso contrario, devuelve false. La igualdad de cada elemento se compara mediante el uso del método equals de Object. Muchas clases redefinen el método equals para realizar las comparaciones de una manera específica a esas clases. Por ejemplo, la clase String declara a equals para comparar los caracteres individuales en los dos objetos String que se están comparando. Si el método equals no se sobrescribe, se utiliza la versión original del método equals heredado de la clase Object.

19.4 La interfaz Collection y la clase Collections

La interfaz Collection es la interfaz raíz en la jerarquía de colecciones, a partir de la cual se derivan las interfaces Set, Queue y List. La interfaz Set define a una colección que no contiene duplicados. La interfaz Queue define a una colección que representa a una línea de espera; por lo general, las inserciones se realizan en la parte final de una cola y las eliminaciones en su parte inicial, aunque pueden especificarse otros órdenes. En las secciones 19.8 y 19.9 hablaremos sobre Queue y Set, respectivamente. La interfaz Collections contiene operaciones masivas (es decir, operaciones que se llevan a cabo en toda una colección) para agregar, borrar, comparar y retener objetos (o elementos) en una colección. Un objeto Collection también puede convertirse en un arreglo. Además, la interfaz Collection proporciona un método que devuelve un objeto Iterator, el cual permite a un programa recorrer toda la colección y eliminar elementos de la misma durante la iteración. En la sección 19.5.1 hablaremos sobre la clase Iterator. Otros métodos de la interfaz Collection permiten a un programa determinar el tamaño de una colección, y si está vacía o no.

Observación de ingeniería de software 19.1 Collection se utiliza comúnmente como un tipo de parámetro de métodos para permitir el procesamiento polimórfico de todos los objetos que implementen a la interfaz Collection.

Observación de ingeniería de software 19.2 La mayoría de las implementaciones de colecciones proporcionan un constructor que toma un argumento Collecpermitiendo así que se construya una nueva colección, la cual contiene los elementos de la colección especificada.

tion,

www.elsolucionario.net

798

Capítulo 19

Colecciones

La clase Collections proporciona métodos static que manipulan las colecciones mediante el polimorfismo. Estos métodos implementan algoritmos para buscar, ordenar, etcétera. En el capítulo 16, Búsqueda y ordenamiento, se describieron e implementaron varios algoritmos de búsqueda y ordenamiento. En la sección 19.6 hablaremos más acerca de los algoritmos disponibles en la clase Collections. También cubriremos los métodos de envoltura de la clase Collections, los cuales nos permiten tratar a una colección como una colección sincronizada (sección 19.2) o una colección no modificable (sección 19.13). Las colecciones no modificables son útiles cuando un cliente de una clase necesita ver los elementos de una colección, pero no se le debe permitir que modifique la colección, agregando y eliminando elementos. Las colecciones sincronizadas son para usarse con una poderosa herramienta conocida como subprocesamiento múltiple (que veremos en el capítulo 23). El subprocesamiento múltiple permite a los programas realizar operaciones en paralelo. Cuando dos o más subprocesos de un programa comparten una colección, existe la probabilidad de que ocurran problemas. Como una breve analogía, considere una intersección de tráfico. No podemos permitir que todos los automóviles accedan a una intersección al mismo tiempo; si lo hiciéramos, ocurrirían accidentes. Por esta razón, se proporcionan semáforos en las intersecciones para controlar el acceso a cada intersección. De manera similar, podemos sincronizar el acceso a una colección para asegurar que sólo un subproceso manipule la colección a la vez. Los métodos de envoltura de sincronización de la clase Collections devuelven las versiones sincronizadas de las colecciones que pueden compartirse entre los subprocesos en un programa.

19.5 Listas Un objeto List (conocido como secuencia) es un objeto Collection ordenado que puede contener elementos duplicados. Al igual que los índices de arreglos, los índices de objetos List empiezan desde cero (es decir, el índice del primer elemento es cero). Además de los métodos de interfaz heredados de Collection, List proporciona métodos para manipular elementos a través de sus índices, para manipular un rango especificado de elementos, para buscar elementos y para obtener un objeto ListIterator para acceder a los elementos. La interfaz List es implementada por varias clases, incluyendo a ArrayList, LinkedList y Vector. La conversión autoboxing ocurre cuando se agregan valores de tipo primitivo a objetos de estas clases, ya que sólo almacenan referencias a objetos. Las clases ArrayList y Vector son implementaciones de un objeto List como arreglos que pueden modificar su tamaño. La clase LinkedList es una implementación de la interfaz List como una lista enlazada. El comportamiento y las herramientas de la clase ArrayList son similares a las de la clase Vector. La principal diferencia entre Vector y ArrayList es que los objetos de la clase Vector están sincronizados de manera predeterminada, mientras que los objetos de la clase ArrayList no. Además, la clase Vector es de Java 1.0, antes de que se agregara el marco de trabajo de colecciones a Java. Como tal, Vector tiene varios métodos que no forman parte de la interfaz List y que no se implementan en la clase ArrayList, pero realizan tareas idénticas. Por ejemplo, los métodos addElement y add de Vector anexan un elemento a un objeto Vector, pero sólo el método add está especificado en la interfaz List y se implementa mediante ArrayList. Las colecciones desincronizadas proporcionan un mejor rendimiento que las sincronizadas. Por esta razón, ArrayList se prefiere comúnmente a Vector en programas que no comparten una colección entre subprocesos.

Tip de rendimiento 19.1 Los objetos ArrayList se comportan igual que los objetos Vector desincronizados y, por lo tanto, se ejecutan con más rapidez que los objetos Vector, ya que los objetos ArrayList no tienen la sobrecarga que implica la sincronización de los subprocesos.

Observación de ingeniería de software 19.3 Los objetos LinkedList pueden usarse para crear pilas, colas, árboles y "colas con dos partes finales" (conocidas en inglés como “deque”). El marco de trabajo de colecciones proporciona implementaciones de algunas de estas estructuras de datos.

En las siguientes tres subsecciones se demuestran las herramientas de List y Collection con varios ejemplos. La sección 19.5.1 se enfoca en eliminar elementos de un objeto ArrayList mediante un objeto Iterator. La sección 19.5.2 se enfoca en ListIterator y varios métodos específicos de List y de LinkedList. La sección 19.5.3 introduce más métodos de List y varios métodos específicos de Vector.

www.elsolucionario.net

19.5

Listas

799

19.5.1 ArrayList e Iterator En la figura 19.3 se utiliza un objeto ArrayList para demostrar varias herramientas de la interfaz Collection. El programa coloca dos arreglos Color en objetos ArrayList y utiliza un objeto Iterator para eliminar los elementos en la segunda colección ArrayList de la primera colección ArrayList. En las líneas 10 a 13 se declaran e inicializan dos variables arreglo String, las cuales se declaran como final, por lo que siempre hacen referencia a estos arreglos. Recuerde que es una buena práctica de programación declarar constantes con las palabras clave static y final. En las líneas 18 y 19 se crean objetos ArrayList y se asignan sus referencias a las variables lista y eliminarLista, respectivamente. Estas dos listas almacenan objetos String. Observe que ArrayList es una clase genérica a partir de Java SE 5, por lo que podemos especificar un argumento de tipo (String en este caso) para indicar el tipo de los elementos en cada lista. Tanto lista como eliminarLista son colecciones de objetos String. En las líneas 22 y 23 se llena lista con objetos String almacenados en el arreglo colores, y en las líneas 26 y 27 se llena eliminarLista con objetos String almacenados en el arreglo eliminarColores, usando el método add de List. En las líneas 32 y 33 se imprime en pantalla cada elemento de lista. En la línea 32 se llama al método size de List para obtener el número de elementos del objeto ArrayList. En la línea 33 se utiliza el método get de List para obtener valores de elementos individuales. En las líneas 32 y 33 se pudo haber usado la instrucción for mejorada. En la línea 36 se hace una llamada al método eliminarColores (líneas 46 a 57), y recibe a lista y eliminarLista como argumentos. El método eliminarColores elimina los objetos String especificados en eliminarLista de la colección lista. En las líneas 41 y 42 se imprimen en pantalla los elementos de lista, una vez que eliminarColores elimina los objetos String especificados en eliminarLista de la lista. El método eliminarColores declara dos parámetros de tipo Collection (línea 47), los cuales contienen cadenas que se van a pasar como argumentos a este método. El método accede a los elementos del primer objeto Collection (coleccion1) mediante un objeto Iterator. En la línea 50 se llama al método iterator de Collection, el cual obtiene un objeto Iterator para el objeto Collection. Observe que las interfaces Collection e Iterator son tipos genéricos. En la condición del ciclo de continuación (línea 53) se hace una llamada al método hasNext de Iterator para determinar si el objeto Collection contiene más elementos. El método hasNext devuelve true si otro elemento existe, y devuelve false en caso contrario. La condición del if en la línea 55 llama al método next de Iterator para obtener una referencia al siguiente elemento, y después utiliza el método contains del segundo objeto Collection (coleccion2) para determinar si coleccion2 contiene el elemento devuelto por next. De ser así, en la línea 56 se hace una llamada al método remove de Iterator para eliminar el elemento del objeto coleccion1 de Collection.

Error común de programación 19.2 Si se modifica una colección mediante uno de sus métodos, después de crear un iterador para esa colección, el iterador se vuelve inválido de manera inmediata; cualquier operación realizada con el iterador después de este punto lanza excepciones ConcurrentModificationException. Por esta razón, se dice que los iteradores son de “falla rápida”.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

// Fig. 19.3: PruebaCollection.java // Uso de la interfaz Collection. import java.util.List; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; public class PruebaCollection { private static final String[] colores = { "MAGENTA", "ROJO", "BLANCO", "AZUL", "CYAN" }; private static final String[] eliminarColores = { "ROJO", "BLANCO", "AZUL" }; // crea objeto ArrayList, le agrega los colores y lo manipula public PruebaCollection()

Figura 19.3 | Demostración de la interfaz Collection mediante un objeto ArrayList. (Parte 1 de 2).

www.elsolucionario.net

800

17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

Capítulo 19

Colecciones

{ List< String > lista = new ArrayList< String >(); List< String > eliminarLista = new ArrayList< String >(); // agrega los elementos en el arreglo colores a la lista for ( String color : colores ) lista.add( color ); // agrega los elementos en eliminarColores a eliminarLista for ( String color : eliminarColores ) eliminarLista.add( color ); System.out.println( "ArrayList: " ); // imprime en pantalla el contenido de la lista for ( int cuenta = 0; cuenta < lista.size(); cuenta++ ) System.out.printf( "%s ", lista.get( cuenta ) ); // elimina los colores contenidos en eliminarLista eliminarColores( lista, eliminarLista ); System.out.println( "\n\nArrayList despues de llamar a eliminarColores: " ); // imprime en pantalla el contenido de la lista for ( String color : lista ) System.out.printf( "%s ", color ); } // fin del constructor de PruebaCollection // elimina de coleccion1 los colores especificados en coleccion2 private void eliminarColores( Collection< String > coleccion1, Collection< String > coleccion2 ) { // obtiene el iterador Iterator< String > iterador = coleccion1.iterator(); // itera mientras la colección tenga elementos while ( iterador.hasNext() ) if ( coleccion2.contains( iterador.next() ) ) iterador.remove(); // elimina el color actual } // fin del método eliminarColores public static void main( String args[] ) { new PruebaCollection(); } // fin de main } // fin de la clase PruebaCollection

ArrayList: MAGENTA ROJO BLANCO AZUL CYAN ArrayList despues de llamar a eliminarColores: MAGENTA CYAN

Figura 19.3 | Demostración de la interfaz Collection mediante un objeto ArrayList. (Parte 2 de 2).

19.5.2 LinkedList En la figura 19.4 se demuestran las operaciones con objetos LinkedList. El programa crea dos objetos LinkedList que contienen objetos String. Los elementos de un objeto List se agregan al otro. Después, todos los objetos String se convierten a mayúsculas, y se elimina un rango de elementos.

www.elsolucionario.net

19.5

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

Listas

801

// Fig. 19.4: PruebaList.java // Uso de objetos LinkList. import java.util.List; import java.util.LinkedList; import java.util.ListIterator; public class PruebaList { private static final String colores[] = { "negro", "amarillo", "verde", "azul", "violeta", "plateado" }; private static final String colores2[] = { "dorado", "blanco", "cafe", "azul", "gris", "plateado" }; // establece y manipula objetos LinkedList public PruebaList() { List< String > lista1 = new LinkedList< String >(); List< String > lista2 = new LinkedList< String >(); // agrega elementos a la lista enlace for ( String color : colores ) lista1.add( color ); // agrega elementos a la lista enlace2 for ( String color : colores2 ) lista2.add( color ); lista1.addAll( lista2 ); // concatena las listas lista2 = null; // libera los recursos imprimirLista( lista1 ); // imprime los elementos de lista1 convertirCadenasAMayusculas( lista1 ); // convierte cadena a mayúsculas imprimirLista( lista1 ); // imprime los elementos de lista1 System.out.print( "\nEliminando elementos 4 a 6..."); eliminarElementos( lista1, 4, 7 ); // elimina los elementos 4 a 7 de la lista imprimirLista( lista1 ); // imprime los elementos de lista1 imprimirListaInversa( lista1 ); // imprime la lista en orden inverso } // fin del constructor de PruebaList // imprime el contenido del objeto List public void imprimirLista( List< String > lista ) { System.out.println( "\nlista: " ); for ( String color : lista ) System.out.printf( "%s ", color ); System.out.println(); } // fin del método imprimirLista // localiza los objetos String y los convierte a mayúsculas private void convertirCadenasAMayusculas( List< String > lista ) { ListIterator< String > iterador = lista.listIterator(); while ( iterador.hasNext() ) { String color = iterador.next();

// obtiene elemento

Figura 19.4 | Objetos List y ListIterator. (Parte 1 de 2).

www.elsolucionario.net

802

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86

Capítulo 19

Colecciones

iterador.set( color.toUpperCase() ); // convierte a mayúsculas } // fin de while } // fin del método convertirCadenasAMayusculas // obtiene sublista y utiliza el método clear para eliminar los elementos de la misma private void eliminarElementos( List< String > lista, int inicio, int fin ) { lista.subList( inicio, fin ).clear(); // elimina los elementos } // fin del método eliminarElementos // imprime la lista inversa private void imprimirListaInversa( List< String > lista ) { ListIterator< String > iterador = lista.listIterator( lista.size() ); System.out.println( "\nLista inversa:" ); // imprime la lista en orden inverso while ( iterador.hasPrevious() ) System.out.printf( "%s ", iterador.previous() ); } // fin del método imprimirListaInversa public static void main( String args[] ) { new PruebaList(); } // fin de main } // fin de la clase PruebaList

lista: negro amarillo verde azul violeta plateado dorado blanco cafe azul gris plateado lista: NEGRO AMARILLO VERDE AZUL VIOLETA PLATEADO DORADO BLANCO CAFE AZUL GRIS PLATEADO Eliminando elementos 4 a 6... lista: NEGRO AMARILLO VERDE AZUL BLANCO CAFE AZUL GRIS PLATEADO Lista inversa: PLATEADO GRIS AZUL CAFE BLANCO AZUL VERDE AMARILLO NEGRO

Figura 19.4 | Objetos List y ListIterator. (Parte 2 de 2). En las líneas 17 y 18 se crean los objetos LinkedList llamados lista1 y lista2 de tipo String. Observe que LinkedList es una clase genérica que tiene un parámetro de tipo, para el cual especificamos el argumento de tipo String en este ejemplo. En las líneas 21 a 26 se hace una llamada al método add de List para anexar elementos de los arreglos colores y colores2 al final de lista1 y lista2, respectivamente. En la línea 28 se hace una llamada al método addAll de List para anexar todos los elementos de lista2 al final de lista1. En la línea 29 se establece lista2 en null, de manera que el objeto LinkedList al que hacía referencia lista2 pueda marcarse para la recolección de basura. En la línea 30 se hace una llamada al método imprimirLista (líneas 42 a 50) para mostrar el contenido de lista1. En la línea 32 se hace una llamada al método convertirCadenaAMayusculas (líneas 53 a 62) para convertir cada elemento String a mayúsculas, y después en la línea 33 se hace una llamada nuevamente a imprimirLista para mostrar los objetos String modificados. En la línea 36 se hace una llamada al método eliminarElementos (líneas 65 a 68) para eliminar los elementos empezando desde el índice 4 hasta, pero no incluyendo, el índice 7 de la lista. En la línea 38 se hace una llamada al método imprimirListaInversa (líneas 71 a 80) para imprimir la lista en orden inverso. El método convertirCadenasAMayusculas (líneas 53 a 62) cambia los elementos String en minúsculas del argumento List por objetos String en mayúsculas. En la línea 55 se hace una llamada al método list-

www.elsolucionario.net

19.5

Listas

803

Iterator de List para obtener un iterador bidireccional (es decir, un iterador que pueda recorrer un objeto Lista hacia delante o hacia atrás) para el objeto List. Observe que ListIterator es una clase genérica. En este ejemplo, el objeto ListIterator contiene objetos String, ya que el método listIterator se llama en un objeto List que contiene objetos String. En la condición del ciclo while (línea 57) se hace una llamada al método hasNext para determinar si el objeto List contiene otro elemento. En la línea 59 se obtiene el siguiente objeto String en el objeto List. En la línea 60 se hace una llamada al método toUpperCase de String para obtener una versión en mayúsculas del objeto String y se hace una llamada al método set de Iterator para reemplazar el objeto String actual al que hace referencia iterador con el objeto String devuelto por el método toUpperCase. Al igual que el método toUpperCase, el método toLowerCase de String devuelve una versión del objeto String en minúsculas. El método eliminarElementos (líneas 65 a 68) elimina un rango de elementos de la lista. En la línea 67 se hace una llamada al método subList de List para obtener una porción del objeto List (lo que se conoce como sublista). La sublista es simplemente otra vista hacia el interior del objeto List desde el que se hace la llamada a subList. El método subList recibe dos argumentos: el índice inicial para la sublista y el índice final. Observe que el índice final no forma parte del rango de la sublista. En este ejemplo, pasamos 4 (en la línea 36) para el índice inicial y 7 para el índice final a subList. La sublista devuelta es el conjunto de elementos con los índices 4 a 6. A continuación, el programa hace una llamada al método clear de List en la sublista para eliminar los elementos que ésta contiene del objeto List. Cualquier cambio realizado a una sublista se hace realmente en el objeto List original. El método imprimirListaInversa (líneas 71 a 80) imprime la lista al revés. En la línea 73 se hace una llamada al método listIterator de List con un argumento que especifica la posición inicial (en nuestro caso, el último elemento en la lista) para obtener un iterador bidireccional para la lista. El método size de List devuelve el número de elementos en el objeto List. En la condición del ciclo while (línea 78) se hace una llamada al método hasPrevious para determinar si hay más elementos mientras se recorre la lista hacia atrás. En la línea 79

se obtiene el elemento anterior de la lista y se envía como salida al flujo de salida estándar. Una característica importante del marco de trabajo de colecciones es la habilidad de manipular los elementos de un tipo de colección (como un conjunto) a través de un tipo de colección distinto (como una lista), sin importar la implementación interna de la colección. Al conjunto de métodos public a través de los cuales se manipulan las colecciones se le conoce como vista. La clase Arrays proporciona el método static asList para ver un arreglo como una colección List (que encapsula el comportamiento similar al de las listas enlazadas que creamos en el capítulo 17). Una vista List permite al programador manipular el arreglo como si fuera una lista. Esto es útil para agregar los elementos en un arreglo a una colección (por ejemplo, un objeto LinkedList) y para ordenar los elementos del arreglo. En el siguiente ejemplo le demostraremos cómo crear un objeto LinkedList con una vista List de un arreglo, ya que no podemos pasar el arreglo a un constructor de LinkedList. En la figura 19.9 se demuestra cómo ordenar elementos de un arreglo con una vista List. Cualquier modificación realizada a través de la vista List cambia el arreglo, y cualquier modificación realizada al arreglo cambia la vista List. La única operación permitida en la vista devuelta por asList es establecer, la cual cambia el valor de la vista y del arreglo de soporte. Cualquier otro intento por cambiar la vista (como agregar o eliminar elementos) produce una excepción UnsupportedOperationException. En la figura 19.5 se utiliza el método asList para ver un arreglo como una colección List, y el método toArray de un objeto List para obtener un arreglo de una colección LinkedList. El programa llama al método asList para crear una vista List de un arreglo, la cual se utiliza después para crear un objeto LinkedList, agrega una serie de cadenas a un objeto LinkedList y llama al método toArray para obtener un arreglo que contiene referencias a esas cadenas. Observe que el instanciamiento de LinkedList (línea 13) indica que es una clase genérica que acepta un argumento de tipo: String, en este ejemplo. En las líneas 13 y 14 se construye un objeto LinkedList de objetos String, el cual contiene los elementos del arreglo colores, y se asigna la referencia LinkedList a enlaces. Observe el uso del método asList de Arrays para devolver una vista del arreglo como un objeto List, y después inicializar el objeto LinkedList con el objeto List. En la línea 16 se hace una llamada al método addLast de LinkedList para agregar "rojo" al final de enlaces. En las líneas 17 y 18 se hace una llamada al método add de LinkedList para agregar "rosa" como el último elemento y "verde" como el elemento en el índice 3 (es decir, el cuarto elemento). Observe que el método addLast (línea 16) es idéntico en función al método add (línea 17). En la línea 19 se hace una llamada al método addFirst de LinkedList para agregar "cyan" como el nuevo primer elemento en el objeto

www.elsolucionario.net

804

Capítulo 19

Colecciones

LinkedList. Las operaciones add están permitidas debido a que operan en el objeto LinkedList, no en la vista devuelta por asList. [Nota: cuando se agrega "cyan" como el primer elemento, "verde" se convierte en el quinto elemento en el objeto LinkedList]. En la línea 22 se hace una llamada al método toArray de List para obtener un arreglo String de enlaces.

El arreglo es una copia de los elementos de la lista; si se modifica el contenido del arreglo no se modifica la lista. El arreglo que se pasa al método toArray debe ser del mismo tipo que se desee que devuelva el método toArray. Si el número de elementos en el arreglo es mayor o igual que el número de elementos en el objeto LinkedList, toArray copia los elementos de la lista en su argumento tipo arreglo y devuelve ese arreglo. Si el objeto LinkedList tiene más elementos que el número de elementos en el arreglo que se pasa a toArray, este método asigna un nuevo arreglo del mismo tipo que recibe como argumento, copia los elementos de la lista en el nuevo arreglo y devuelve este nuevo arreglo.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

// Fig. 19.5: UsoToArray.java // Uso del método toArray. import java.util.LinkedList; import java.util.Arrays; public class UsoToArray { // el constructor crea un objeto LinkedList, le agrega elementos y lo convierte en arreglo public UsoToArray() { String colores[] = { "negro", "azul", "amarillo" }; LinkedList< String > enlaces = new LinkedList< String >( Arrays.asList( colores ) ); enlaces.addLast( "rojo" ); enlaces.add( "rosa" ); enlaces.add( 3, "verde" ); enlaces.addFirst( "cyan" );

// // // //

lo lo lo lo

agrega agrega agrega agrega

como último elemento al final en el 3er índice como primer elemento

// obtiene los elementos de LinkedList como un arreglo colores = enlaces.toArray( new String[ enlaces.size() ] ); System.out.println( "colores: " ); for ( String color : colores ) System.out.println( color ); } // fin del constructor de UsoToArray public static void main( String args[] ) { new UsoToArray(); } // fin de main } // fin de la clase UsoToArray

colores: cyan negro azul amarillo verde rojo rosa

Figura 19.5 | Método toArray de List.

www.elsolucionario.net

19.5

Listas

805

Error común de programación 19.3 Pasar un arreglo que contenga datos al método toArray puede crear errores lógicos. Si el número de elementos en el arreglo es menor que el número de elementos en la lista en la que se llama a toArray, se asigna un nuevo arreglo para almacenar los elementos de la lista (sin preservar los elementos del arreglo). Si el número de elementos en el arreglo es mayor que el número de elementos en la lista, los elementos del arreglo (empezando en el índice cero) se sobrescriben con los elementos de la lista. Los elementos de arreglos que no se sobrescriben retienen sus valores.

19.5.3 Vector Al igual que ArrayList, la clase Vector proporciona las herramientas de las estructuras de datos tipo arreglo que pueden cambiar su tamaño en forma dinámica. Recuerde que el comportamiento y las herramientas de la clase ArrayList son similares a las de la clase Vector, excepto que los objetos ArrayList no proporcionan sincronización de manera predeterminada. Aquí veremos la clase Vector, principalmente debido a que es la superclase de la clase Stack, la cual se presenta en la sección 19.7. En cualquier momento, un objeto Vector contiene un número de elementos que es menor o igual que su capacidad. La capacidad es el espacio que se ha reservado para los elementos del objeto Vector. Si un Vector requiere capacidad adicional, crece en base a un incremento de capacidad que el programador especifica, o mediante un incremento de capacidad predeterminado. Si no especificamos un incremento de capacidad, o si especificamos uno que sea menor o igual a cero, el sistema duplicará el tamaño de un objeto Vector cada vez que se necesite capacidad adicional.

Tip de rendimiento 19.2 Insertar un elemento en un objeto Vector cuyo tamaño actual sea menor que su capacidad es una operación relativamente rápida.

Tip de rendimiento 19.3 Insertar un elemento en un objeto Vector que necesita crecer más para dar cabida al nuevo elemento es una operación relativamente lenta.

Tip de rendimiento 19.4 El incremento de capacidad predeterminado duplica el tamaño del objeto Vector. Esto puede parecer un desperdicio de almacenamiento, pero en realidad es una manera eficiente para que muchos objetos Vector aumenten rápidamente al “tamaño correcto”. Esta operación es mucho más eficiente que aumentar el objeto Vector cada vez sólo el espacio necesario para contener un solo elemento. La desventaja es que el objeto Vector podría ocupar más espacio de lo requerido. Éste es un clásico ejemplo de la concesión entre espacio y tiempo.

Tip de rendimiento 19.5 Si el almacenamiento es primordial, use el método trimToSize de Vector para recortar la capacidad del objeto Vector a su tamaño exacto. Esta operación optimiza el uso que da un objeto Vector al almacenamiento. Sin embargo, al agregar otro elemento al objeto Vector, éste se verá forzado a crecer en forma dinámica (de nuevo, una operación relativamente lenta); al recortar su tamaño mediante trimToSize, no queda espacio para que crezca.

En la figura 19.6 se demuestra la clase Vector y varios de sus métodos. Para obtener información completa acerca de la clase Vector, visite el sitio Web java.sun.com/javase/6/docs/api/java/util/Vector.html. El constructor de la aplicación crea un objeto Vector (línea 12) de tipo String, con una capacidad inicial de 10 elementos y un incremento de capacidad de cero (los valores predeterminados para un objeto Vector). Observe que Vector es una clase genérica, la cual recibe un argumento que especifica el tipo de los elementos almacenados en el objeto Vector. Un incremento de capacidad de cero indica que este objeto Vector duplicará su tamaño cada vez que necesite crecer, para dar cabida a más elementos. La clase Vector proporciona otros tres constructores. El constructor que recibe un argumento entero crea un objeto Vector vacío con la capacidad inicial especificada por ese argumento. El constructor que recibe dos argumentos crea un objeto Vector con la capacidad inicial especificada por el primer argumento, y el incremento de capacidad especificado por el segundo argumento. Cada vez que el vector necesita crecer, agrega espacio para el número especificado de elementos en

www.elsolucionario.net

806

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

Capítulo 19

Colecciones

// Fig. 19.6: PruebaVector.java // Uso de la clase Vector. import java.util.Vector; import java.util.NoSuchElementException; public class PruebaVector { private static final String colores[] = { "rojo", "blanco", "azul" }; public PruebaVector() { Vector< String > vector = new Vector< String >(); imprimirVector( vector ); // imprime el vector // agrega elementos al vector for ( String color : colores ) vector.add( color ); imprimirVector( vector ); // imprime el vector // imprime los elementos primero y último try { System.out.printf( "Primer elemento: %s\n", vector.firstElement()); System.out.printf( "Ultimo elemento: %s\n", vector.lastElement() ); } // fin de try // atrapa la excepción si el vector está vacío catch ( NoSuchElementException excepcion ) { excepcion.printStackTrace(); } // fin de catch // ¿el vector contiene "rojo"? if ( vector.contains( "rojo" ) ) System.out.printf( "\se encontro \"rojo\" en el indice %d\n\n", vector.indexOf( "rojo" ) ); else System.out.println( "\no se encontro \"rojo\"\n" ); vector.remove( "rojo" ); // elimina la cadena "rojo" System.out.println( "se elimino \"rojo\" ); imprimirVector( vector ); // imprime el vector // ¿el vector contiene "rojo" después de la operación de eliminación? if ( vector.contains( "rojo" ) ) System.out.printf( “se encontro \"rojo\" en el indice %d\n", vector.indexOf( "rojo" ) ); else System.out.println( "no se encontro \"rojo\"" ); // imprime el tamaño y la capacidad del vector System.out.printf( "\nTamanio: %d\nCapacidad: %d\n", vector.size(), vector.capacity() ); } // fin del constructor de PruebaVector private void imprimirVector( Vector< String > vectorAImprimir ) { if ( vectorAImprimir.isEmpty() ) System.out.print( "el vector esta vacio" ); // vectorAImprimir está vacío

Figura 19.6 | La clase Vector del paquete java.util. (Parte 1 de 2).

www.elsolucionario.net

19.5

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76

Listas

807

else // itera a través de los elementos { System.out.print( "el vector contiene: " ); // imprime los elementos for ( String elemento : vectorAImprimir ) System.out.printf( "%s ", elemento ); } // fin de else System.out.println( "\n" ); } // fin del método imprimirVector public static void main( String args[] ) { new PruebaVector(); // crea objeto y llama a su constructor } // fin de main } // fin de la clase PruebaVector

el vector esta vacio el vector contiene: rojo blanco azul Primer elemento: rojo Ultimo elemento: azul se encontro "rojo" en el indice 0 se elimino "rojo" el vector contiene: blanco azul no se encontro "rojo" Tamanio: 2 Capacidad: 10

Figura 19.6 | La clase Vector del paquete java.util. (Parte 2 de 2).

el incremento de capacidad. El constructor que recibe un objeto Collection crea una copia de los elementos de una colección y los almacena en el objeto Vector. En la línea 17 se hace una llamada al método add de Vector para agregar objetos (en este programa son de tipo String) al final del objeto Vector. Si es necesario, el objeto Vector incrementa su capacidad para dar cabida al nuevo elemento. La clase Vector también proporciona un método add que recibe dos argumentos. Este método recibe un objeto y un entero, e inserta el objeto en el índice especificado en el objeto Vector. El método set reemplaza el elemento en una posición especificada en el objeto Vector, con un elemento especificado. El método insertElementAt proporciona la misma funcionalidad que el método add que recibe dos argumentos, excepto que el orden de los parámetros se invierte. En la línea 24 se hace una llamada al método firstElement de Vector para devolver una referencia al primer elemento en el objeto Vector. En la línea 25 se hace una llamada al método lastElement de Vector para devolver una referencia al último elemento en el objeto Vector. Cada uno de estos métodos lanza una excepción NoSuchElementException si no hay elementos en el objeto Vector cuando se hace la llamada al método. En la línea 34 se hace una llamada al método contains de Vector para determinar si el objeto Vector contiene "rojo". El método devuelve true si su argumento está en el objeto Vector; en caso contrario, el método devuelve false. El método contains utiliza el método equals de Object para determinar si la clave de búsqueda es igual a uno de los elementos del objeto Vector. Muchas clases sobrescriben el método equals para realizar las comparaciones de una manera específica para esas clases. Por ejemplo, la clase String declara a equals para comparar los caracteres individuales en los dos objetos String que se van a comparar. Si el método equals no se sobrescribe, se utiliza la versión original del método equals heredada de la clase Object.

www.elsolucionario.net

808

Capítulo 19

Colecciones

Error común de programación 19.4 Sin sobrescribir el método equals, el programa realiza comparaciones mediante el operador = = para determinar si dos referencias se refieren al mismo objeto en la memoria.

En la línea 36 se hace una llamada al método indexOf para determinar el índice de la primera ubicación en el objeto Vector que contenga el argumento. El método devuelve -1 si el argumento no se encuentra en el objeto Vector. Una versión sobrecargada de este método recibe un segundo argumento que especifica el índice en el objeto Vector en el que debe empezar la búsqueda.

Tip de rendimiento 19.6 Los métodos de Vector llamados contains e indexOf realizan búsquedas lineales en el contenido de un objeto Vector. Estas búsquedas son ineficientes para objetos Vector más grandes. Si un programa busca frecuentemente elementos en una colección, considere utilizar una de las implementaciones de Map de la API Collection de Java (sección 19.10), la cual proporciona herramientas de búsqueda de alta velocidad.

En la línea 40 se hace una llamada al método remove de Vector para eliminar del objeto Vector la primera ocurrencia de su argumento. Este método devuelve true si encuentra el elemento en el objeto Vector; en caso contrario, el método devuelve false. Si se elimina el elemento, todos los elementos después de ese elemento en el objeto Vector se desplazan una posición hacia el inicio del objeto Vector, para llenar la posición del elemento eliminado. La clase Vector también proporciona el método removeAllElements para eliminar todos los elementos de un objeto Vector, y el método removeElementAt para eliminar el elemento en un índice especificado. En las líneas 52 y 53 se utilizan los métodos size y capacity de Vector para determinar el número de elementos actuales en el Vector, y el número de elementos que pueden almacenarse en el Vector sin asignar más memoria, respectivamente. En la línea 58 se hace una llamada al método isEmpty de Vector para determinar si el objeto Vector está vacío. El método devuelve true si no hay elementos en el objeto Vector; en caso contrario, el método devuelve false. En las líneas 65 y 66 se utiliza la instrucción for mejorada para imprimir todos los elementos en el vector. Entre los métodos introducidos en la figura 19.6, firstElement, lastElement y capacity sólo se pueden utilizar con Vector. Los otros métodos (por ejemplo, add, contains, indexOf, remove, size e isEmpty) se declaran mediante List, lo cual significa que cualquier clase que implemente a List (como Vector) puede utilizarlos.

19.6 Algoritmos de las colecciones El marco de trabajo de colecciones cuenta con varios algoritmos de alto rendimiento para manipular los elementos de una colección. Estos algoritmos se implementan como métodos static de la clase Collections (figura 19.7). Los algoritmos sort, binarySearch, reverse, shuffle, fill y copy operan con objetos List. Los algoritmos min, max, addAll, frequency y disjoint operan con objetos Collections.

Algoritmo

Descripción

sort

Ordena los elementos de un objeto List.

binarySearch

Localiza un objeto en un objeto List.

reverse

Invierte los elementos de un objeto List.

shuffle

Ordena al azar los elementos de un objeto List.

fill

Establece cada elemento de un objeto List para que haga referencia a un objeto especificado.

copy

Copia referencias de un objeto List a otro.

min

Devuelve el elemento más pequeño en un objeto Collection.

Figura 19.7 | Algoritmos de colecciones. (Parte 1 de 2).

www.elsolucionario.net

19.6

Algoritmos de las colecciones

Algoritmo

Descripción

max

Devuelve el elemento más grande en un objeto Collection.

addAll

Anexa todos los elementos en un arreglo a una colección.

frequency

Calcula cuántos elementos en la colección son iguales al elemento especificado.

disjoint

Determina si dos colecciones no tienen elementos en común.

809

Figura 19.7 | Algoritmos de colecciones. (Parte 2 de 2).

Observación de ingeniería de software 19.4 Los algoritmos del marco de trabajo de colecciones son polimórficos. Es decir, cada algoritmo puede operar en objetos que implementen interfaces específicas, sin importar sus implementaciones subyacentes.

19.6.1 El algoritmo sort El algoritmo sort ordena los elementos de un objeto List, el cual debe implementar a la interfaz Comparable. El orden se determina en base al orden natural del tipo de los elementos, según su implementación mediante el método compareTo de ese objeto. El método compareTo está declarado en la interfaz Comparable y algunas veces se le conoce como el método de comparación natural. La llamada a sort puede especificar como segundo argumento un objeto Comparator, para determinar un ordenamiento alterno de los elementos.

Ordenamiento ascendente En la figura 19.8 se utiliza el algoritmo sort para ordenar los elementos de un objeto List en forma ascendente (línea 20). Recuerde que List es un tipo genérico y acepta un argumento de tipo, el cual especifica el tipo de elemento de lista; en la línea 15 se declara a lista como un objeto List de objetos String. Observe que en las líneas 18 y 23 se utiliza una llamada implícita al método toString de lista para imprimir el contenido de la lista en el formato que se muestra en las líneas segunda y cuarta de los resultados.

Ordenamiento descendente En la figura 19.9 se ordena la misma lista de cadenas utilizadas en la figura 19.8, en orden descendente. El ejemplo introduce la interfaz Comparator, la cual se utiliza para ordenar los elementos de un objeto Collection en un orden distinto. En la línea 21 se hace una llamada al método sort de Collections para ordenar el objeto List en orden descendente. El método static reverseOrder de Collections devuelve un objeto Comparator que ordena los elementos de la colección en orden inverso.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

// Fig. 19.8: Ordenamiento1.java // Uso del algoritmo sort. import java.util.List; import java.util.Arrays; import java.util.Collections; public class Ordenamiento1 { private static final String palos[] = { "Corazones", "Diamantes", "Bastos", "Espadas" }; // muestra los elementos del arreglo public void imprimirElementos() { List< String > lista = Arrays.asList( palos ); // crea objeto List

Figura 19.8 | El método sort de Collections. (Parte 1 de 2).

www.elsolucionario.net

810

16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

Capítulo 19

Colecciones

// imprime lista System.out.printf( "Elementos del arreglo desordenados:\n%s\n", lista ); Collections.sort( lista ); // ordena ArrayList // imprime lista System.out.printf( "Elementos del arreglo ordenados:\n%s\n", lista ); } // fin del método imprimirElementos public static void main( String args[] ) { Ordenamiento1 orden1 = new Ordenamiento1(); orden1.imprimirElementos(); } // fin de main } // fin de la clase Ordenamiento1

Elementos del arreglo desordenados: [Corazones, Diamantes, Bastos, Espadas] Elementos del arreglo ordenados: [Bastos, Corazones, Diamantes, Espadas]

Figura 19.8 | El método sort de Collections. (Parte 2 de 2).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

// Fig. 19.9: Ordenamiento2.java // Uso de un objeto Comparator con el algoritmo sort. import java.util.List; import java.util.Arrays; import java.util.Collections; public class Ordenamiento2 { private static final String palos[] = { "Corazones", "Diamantes", "Bastos", "Espadas" }; // imprime los elementos del objeto List public void imprimirElementos() { List< String > lista = Arrays.asList( palos ); // crea objeto List // imprime los elementos del objeto List System.out.printf( "Elementos del arreglo desordenados:\n%s\n", lista ); // ordena en forma descendente, utilizando un comparador Collections.sort( lista, Collections.reverseOrder() ); // imprime los elementos del objeto List System.out.printf( "Elementos de lista ordenados:\n%s\n", lista ); } // fin del método imprimirElementos public static void main( String args[] ) { Ordenamiento2 ordenamiento = new Ordenamiento2(); ordenamiento.imprimirElementos(); } // fin de main } // fin de la clase Ordenamiento2

Figura 19.9 | El método sort de Collections con un objeto Comparator. (Parte 1 de 2).

www.elsolucionario.net

19.6

Algoritmos de las colecciones

811

Elementos del arreglo desordenados: [Corazones, Diamantes, Bastos, Espadas] Elementos de lista ordenados: [Espadas, Diamantes, Corazones, Bastos]

Figura 19.9 | El método sort de Collections con un objeto Comparator. (Parte 2 de 2).

Ordenamiento mediante un objeto Comparator En la figura 19.10 se crea una clase Comparator personalizada, llamada ComparadorTiempo, la cual implementa a la interfaz Comparator para comparar dos objetos Tiempo2. La clase Tiempo2, declarada en la figura 8.5, representa tiempos con horas, minutos y segundos. La clase ComparadorTiempo implementa a la interfaz Comparator, un tipo genérico que recibe un argumento (en este caso, Tiempo2). El método compare (líneas 7 a 26) realiza comparaciones entre objetos Tiempo2. En la línea 9 se comparan las dos horas de los objetos Tiempo2. Si las horas son distintas (línea 12), entonces devolvemos este valor. Si el valor es positivo, entonces la primera hora es mayor que la segunda y el primer tiempo es mayor que el segundo. Si este valor es negativo, entonces la primera hora es menor que la segunda y el primer tiempo es menor que el segundo. Si este valor es cero, las horas son iguales y debemos evaluar los minutos (y tal vez los segundos) para determinar cuál tiempo es mayor. En la figura 19.11 se ordena una lista mediante el uso de la clase Comparator personalizada, llamada ComparadorTiempo. En la línea 11 se crea un objeto ArrayList de objetos Tiempo2. Recuerde que ArrayList y List son tipos genéricos y aceptan un argumento de tipo que especifica el tipo de los elementos de la colección. En las líneas 13 a 17 se crean cinco objetos Tiempo2 y se agregan a esta lista. En la línea 23 se hace una llamada al método sort, y le pasamos un objeto de nuestra clase ComparadorTiempo (figura 19.10).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

// Fig. 19.10: ComparadorTiempo.java // Clase Comparator personalizada que compara dos objetos Tiempo2. import java.util.Comparator; public class ComparadorTiempo implements Comparator< Tiempo2 > { public int compare( Tiempo2 tiempo1, Tiempo2 tiempo2 ) { int compararHora = tiempo1.obtenerHora() - tiempo2.obtenerHora(); // compara la hora // evalúa la hora primero if ( compararHora != 0 ) return compararHora; int comparaMinuto = tiempo1.obtenerMinuto() - tiempo2.obtenerMinuto(); // compara el minuto // después evalúa el minuto if ( comparaMinuto != 0 ) return comparaMinuto; int compararSegundo = tiempo1.obtenerSegundo() - tiempo2.obtenerSegundo(); // compara el segundo return compararSegundo; // devuelve el resultado de comparar los segundos } // fin del método compare } // fin de la clase ComparadorTiempo

Figura 19.10 | Clase Comparator personalizada que compara dos objetos Tiempo2.

www.elsolucionario.net

812

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

Capítulo 19

Colecciones

// Fig. 19.11: Ordenamiento3.java // Ordena una lista usando la clase Comparator personalizada ComparadorTiempo. import java.util.List; import java.util.ArrayList; import java.util.Collections; public class Ordenamiento3 { public void imprimirElementos() { List< Tiempo2 > lista = new ArrayList< Tiempo2 >(); // crea objeto List lista.add( lista.add( lista.add( lista.add( lista.add(

new new new new new

Tiempo2( 6, 24, 34 ) ); Tiempo2( 18, 14, 58 ) ); Tiempo2( 6, 05, 34 ) ); Tiempo2( 12, 14, 58 ) ); Tiempo2( 6, 24, 22 ) );

// imprime los elementos del objeto List System.out.printf( "Elementos del arreglo desordenados:\n%s\n", lista ); // ordena usando un comparador Collections.sort( lista, new ComparadorTiempo() ); // imprime los elementos del objeto List System.out.printf( "Elementos de la lista ordenados:\n%s\n", lista ); } // fin del método imprimirElementos public static void main( String args[] ) { Ordenamiento3 ordenamiento3 = new Ordenamiento3(); ordenamiento3.imprimirElementos(); } // fin de main } // fin de la clase Ordenamiento3

Elementos del arreglo desordenados: [6:24:34 AM, 6:14:58 PM, 6:05:34 AM, 12:14:58 PM, 6:24:22 AM] Elementos de la lista ordenados: [6:05:34 AM, 6:24:22 AM, 6:24:34 AM, 12:14:58 PM, 6:14:58 PM]

Figura 19.11 | El método sort de Collections con un objeto Comparator personalizado.

19.6.2 El algoritmo shuffle El algoritmo shuffle ordena al azar los elementos de un objeto List. En el capítulo 7 presentamos una simulación para barajar y repartir cartas, en la que se utiliza un ciclo para barajar un mazo de cartas. En la figura 19.12, utilizamos el algoritmo shuffle para barajar un mazo de objetos Carta que podría usarse en un simulador de juego de cartas. La clase Carta (líneas 8 a 41) representa a una carta en un mazo de cartas. Cada Carta tiene una cara y un palo. Las líneas 10 a 12 declaran dos tipos enum (Cara y Palo) que representan la cara y el palo de la carta, respectivamente. El método toString (líneas 37 a 40) devuelve un objeto String que contiene la cara y el palo de la Carta, separados por la cadena " de ". Cuando una constante enum se convierte en una cadena, el identificador de la constante se utiliza como la representación de cadena. Por lo general, utilizamos letras mayúsculas para las constantes enum. En este ejemplo, optamos por usar letras mayúsculas sólo para la primera letra de cada constante enum, porque queremos que la carta se muestre con letras iniciales mayúsculas para la cara y el palo (por ejemplo, "As de Bastos"). En las líneas 55 a 62 se llena el arreglo mazo con cartas que tienen combinaciones únicas de cara y palo. Tanto Cara como Palo son tipos public static enum de la clase Carta. Para usar estos tipos enum fuera de la

www.elsolucionario.net

19.6

Algoritmos de las colecciones

813

clase Carta, debe calificar el nombre de cada tipo enum con el nombre de la clase en la que reside (es decir, Carta) y un separador punto (.). Así, en las líneas 55 y 57 se utilizan Carta.Palo y Carta.Cara para declarar las variables de control de las instrucciones for. Recuerde que el método values de un tipo enum devuelve un arreglo que contiene todas las constantes del tipo enum. En las líneas 55 a 62 se utilizan instrucciones for mejoradas para construir 52 nuevos objetos Carta.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51

// Fig. 19.12: MazoDeCartas.java // Uso del algoritmo shuffle. import java.util.List; import java.util.Arrays; import java.util.Collections; // clase para representar un objeto Carta en un mazo de cartas class Carta { public static enum Cara { As, Dos, Tres, Cuatro, Cinco, Seis, Siete, Ocho, Nueve, Diez, Joto, Quina, Rey }; public static enum Palo { Bastos, Diamantes, Corazones, Espadas }; private final Cara cara; // cara de la carta private final Palo palo; // palo de la carta // constructor con dos argumentos public Carta( Cara caraCarta, Palo paloCarta ) { cara = caraCarta; // inicializa la cara de la carta palo = paloCarta; // inicializa el palo de la carta } // fin del constructor de Carta con dos argumentos // devuelve la cara de la carta public Cara obtenerCara() { return cara; } // fin del método obtenerCara // devuelve el palo de la Carta public Palo obtenerPalo() { return palo; } // fin del método obtenerPalo // devuelve la representación String de la Carta public String toString() { return String.format( "%s de %s", cara, palo ); } // fin del método toString } // fin de la clase Carta // declaración de la clase MazoDeCartas public class MazoDeCartas { private List< Carta > lista; // declara objeto List que almacenará los objetos Carta // establece mazo de objetos Carta y baraja public MazoDeCartas() { Carta[] mazo = new Carta[ 52 ];

Figura 19.12 | Barajar y repartir cartas con el método shuffle de Collections. (Parte 1 de 2).

www.elsolucionario.net

814

52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82

Capítulo 19

Colecciones

int cuenta = 0; // número de cartas // llena el mazo con objetos Carta for ( Carta.Palo palo : Carta.Palo.values() ) { for ( Carta.Cara cara : Carta.Cara.values() ) { mazo[ cuenta ] = new Carta( cara, palo ); cuenta++; } // fin de for } // fin de for lista = Arrays.asList( mazo ); // obtiene objeto List Collections.shuffle( lista ); // baraja el mazo } // fin del constructor de MazoDeCartas // imprime el mazo public void imprimirCartas() { // muestra las 52 cartas en dos columnas for ( int i = 0; i < lista.size(); i++ ) System.out.printf( "%-20s%s", lista.get( i ), ( ( i + 1 ) % 2 == 0 ) ? "\n" : "\t" ); } // fin del método imprimirCartas public static void main( String args[] ) { MazoDeCartas cartas = new MazoDeCartas(); cartas.imprimirCartas(); } // fin de main } // fin de la clase MazoDeCartas

Ocho de Bastos As de Corazones Quina de Espadas Cuatro de Corazones Dos de Espadas Nueve de Bastos Joto de Bastos Nueve de Diamantes Cinco de Corazones Dos de Bastos Diez de Bastos Cinco de Bastos Diez de Espadas Seis de Bastos Siete de Espadas Rey de Espadas As de Diamantes Joto de Diamantes Quina de Diamantes Dos de Corazones Cinco de Espadas Siete de Diamantes Quina de Bastos Joto de Espadas As de Bastos Ocho de Espadas

Siete de Corazones Nueve de Espadas Ocho de Corazones Tres de Diamantes Seis de Espadas Nueve de Corazones Dos de Diamantes Rey de Corazones Ocho de Diamantes Diez de Diamantes Seis de Corazones Tres de Bastos Tres de Espadas Tres de Corazones As de Espadas Joto de Corazones Seis de Diamantes Cinco de Diamantes Cuatro de Espadas Rey de Bastos Cuatro de Bastos Cuatro de Diamantes Diez de Corazones Quina de Corazones Siete de Bastos Rey de Diamantes

Figura 19.12 | Barajar y repartir cartas con el método shuffle de Collections. (Parte 2 de 2).

www.elsolucionario.net

19.6

Algoritmos de las colecciones

815

La acción de barajar las cartas ocurre en la línea 65, en la cual se hace una llamada al método static shuffle de la clase Collections para barajar los elementos del arreglo. El método shuffle requiere un argumento List, por lo que debemos obtener una vista List del arreglo antes de poder barajarlo. En la línea 64 se invoca el método static asList de la clase Arrays para obtener una vista List del arreglo mazo. El método imprimirCartas (líneas 69 a 75) muestra el mazo de cartas en dos columnas. En cada iteración del ciclo, en las líneas 73 y 74 se imprime una carta justificada a la izquierda, en un campo de 20 caracteres seguido de una nueva línea o de una cadena vacía, con base en el número de cartas mostradas hasta ese momento. Si el número de cartas es par, se imprime una nueva línea; en caso contrario, se imprime un tabulador.

19.6.3 Los algoritmos reverse, fill, copy, max y min La clase Collections proporciona algoritmos para invertir, llenar y copiar objetos List. El algoritmo reverse invierte el orden de los elementos en un objeto List y el algoritmo fill sobrescribe los elementos en un objeto List con un valor especificado. La operación fill es útil para reinicializar un objeto List. El algoritmo copy recibe dos argumentos: un objeto List de destino y un objeto List de origen. Cada elemento del objeto List de origen se copia al objeto List de destino. El objeto List de destino debe tener cuando menos la misma longitud que el objeto List de origen; de lo contrario, se producirá una excepción IndexOutOfBoundsException. Si el objeto List de destino es más largo, los elementos que no se sobrescriban permanecerán sin cambio. Cada uno de los algoritmos que hemos visto hasta ahora opera en objetos List. Los algoritmos min y max operan en cualquier objeto Collection. El algoritmo min devuelve el elemento más pequeño en un objeto Collection y el algoritmo max devuelve el elemento más grande en un objeto Collection. Ambos algoritmos pueden llamarse con un objeto Comparator como segundo argumento, para realizar comparaciones personalizadas entre objetos, como el objeto ComparadorTiempo en la figura 19.11. En la figura 19.13 se demuestra el uso de los algoritmos reverse, fill, copy, min y max. Observe que se declara el tipo genérico List para almacenar objetos Character. En la línea 24 se hace una llamada al método reverse de Collections para invertir el orden de lista. El método reverse recibe un argumento List. En este caso, lista es una vista List del arreglo letras. Ahora el arreglo letras tiene sus elementos en orden inverso. En la línea 28 se copian los elementos de lista en copiaLista, usando el método copy de Collections. Los cambios a copiaLista no cambian a letras, ya que copiaLista es un objeto List separado que no es una vista List para letras. El método copy requiere dos argumentos List. En la línea 32 se hace una llamada al método fill de Collections para colocar la cadena "R" en cada elemento de lista. Como lista es una vista List de letras, esta operación cambia cada elemento en letras a "R". El método fill requiere un objeto List como primer argumento, y un objeto Object como segundo argumento. En las líneas 45 y 46 se hace una llamada a los métodos max y min de Collections para buscar el elemento más grande y más pequeño de la colección, respectivamente. Recuerde que un objeto List es un objeto Collection, por lo que en las líneas 45 y 46 se puede pasar un objeto List a los métodos max y min.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

// Fig. 19.13: Algoritmos1.java // Uso de los algoritmos reverse, fill, copy, min y max. import java.util.List; import java.util.Arrays; import java.util.Collections; public class Algoritmos1 { private Character[] letras = { 'P', 'C', 'M' }; private Character[] copiaLetras; private List< Character > lista; private List< Character > copiaLista; // crea un objeto List y lo manipula con los métodos de Collections public Algoritmos1() { lista = Arrays.asList( letras ); // obtiene el objeto List

Figura 19.13 | Los métodos reverse, fill, copy, max y min de Collections. (Parte 1 de 2).

www.elsolucionario.net

816

18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53

Capítulo 19

Colecciones

copiaLetras = new Character[ 3 ]; copiaLista = Arrays.asList( copiaLetras ); // vista List de copiaLetras System.out.println( "Lista inicial: " ); imprimir( lista ); Collections.reverse( lista ); // invierte el orden System.out.println( "\nDespues de llamar a reverse: " ); imprimir( lista ); Collections.copy( copiaLista, lista ); // copia el objeto List System.out.println( "\nDespues de copy: " ); imprimir( copiaLista ); Collections.fill( lista, 'R' ); // llena la lista con Rs System.out.println( "\nDespues de llamar a fill: " ); imprimir( lista ); } // fin del constructor de Algoritmos1 // imprime la información del objeto List private void imprimir( List< Character > refLista ) { System.out.print( "La lista es: " ); for ( Character elemento : refLista ) System.out.printf( "%s ", elemento ); System.out.printf( "\nMax: %s", Collections.max( refLista ) ); System.out.printf( " Min: %s\n", Collections.min( refLista ) ); } // fin del método imprimir public static void main( String args[] ) { new Algoritmos1(); } // fin de main } // fin de la clase Algoritmos1

Lista inicial: La lista es: P C M Max: P Min: C Despues de llamar a reverse: La lista es: M C P Max: P Min: C Despues de copy: La lista es: M C P Max: P Min: C Despues de llamar a fill: La lista es: R R R Max: R Min: R

Figura 19.13 | Los métodos reverse, fill, copy, max y min de Collections. (Parte 2 de 2).

19.6.4 El algoritmo binarySearch En la sección 16.2.2 estudiamos el algoritmo de búsqueda binaria, de alta velocidad. Este algoritmo se incluye en el marco de trabajo de colecciones de Java como un método static de la clase Collections. El algoritmo binarySearch localiza un objeto en un objeto List (es decir, un objeto LinkedList, Vector o ArrayList). Si

www.elsolucionario.net

19.6

Algoritmos de las colecciones

817

se encuentra el objeto, se devuelve el índice de ese objeto. Si no se encuentra el objeto, binarySearch devuelve un valor negativo. El algoritmo binarySearch determina este valor negativo calculando primero el punto de inserción y cambiando el signo del punto de inserción a negativo. Después, binarySearch resta uno al punto de inserción para obtener el valor de retorno, el cual garantiza que el método binarySearch devolverá números positivos (>= 0), sí y sólo si se encuentra el objeto. Si varios elementos en la lista coinciden con la clave de búsqueda, no hay garantía de que uno se localice primero. En la figura 19.14 se utiliza el algoritmo binarySearch para buscar una serie de cadenas en un objeto ArrayList.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49

// Fig. 19.14: PruebaBusquedaBinaria.java // Uso del algoritmo binarySearch. import java.util.List; import java.util.Arrays; import java.util.Collections; import java.util.ArrayList; public class PruebaBusquedaBinaria { private static final String colores[] = { "rojo", "blanco", "azul", "negro", "amarillo", "morado", "carne", "rosa" }; private List< String > lista; // referencia ArrayList // crea, ordena e imprime la lista public PruebaBusquedaBinaria() { lista = new ArrayList< String >( Arrays.asList( colores ) ); Collections.sort( lista ); // ordena el objeto ArrayList System.out.printf( "ArrayList ordenado: %s\n", lista ); } // fin del constructor de PruebaBusquedaBinaria // busca varios valores en la lista private void buscar() { imprimirResultadosBusqueda( colores[ 3 ] ); // primer elemento imprimirResultadosBusqueda( colores[ 0 ] ); // elemento medio imprimirResultadosBusqueda( colores[ 7 ] ); // último elemento imprimirResultadosBusqueda( "aqua" ); // debajo del menor imprimirResultadosBusqueda( "gris" ); // no existe imprimirResultadosBusqueda( "verdeazulado" ); // no existe } // fin del método buscar // método ayudante para realizar búsquedas private void imprimirResultadosBusqueda( String clave ) { int resultado = 0; System.out.printf( "\nBuscando: %s\n", clave ); resultado = Collections.binarySearch( lista, clave ); if ( resultado >= 0 ) System.out.printf( "Se encontro en el indice %d\n", resultado ); else System.out.printf( "No se encontro (%d)\n",resultado ); } // fin del método imprimirResultadosBusqueda public static void main( String args[] ) { PruebaBusquedaBinaria pruebaBusquedaBinaria = new PruebaBusquedaBinaria();

Figura 19.14 | El método binarySearch de Collections. (Parte 1 de 2).

www.elsolucionario.net

818

50 51 52

Capítulo 19

Colecciones

pruebaBusquedaBinaria.buscar(); } // fin de main } // fin de la clase PruebaBusquedaBinaria

ArrayList ordenado: [amarillo, azul, blanco, carne, morado, negro, rojo, rosa] Buscando: negro Se encontro en el indice 5 Buscando: rojo Se encontro en el indice 6 Buscando: rosa Se encontro en el indice 7 Buscando: aqua No se encontro (-2) Buscando: gris No se encontro (-5) Buscando: verdeazulado No se encontro (-9)

Figura 19.14 | El método binarySearch de Collections. (Parte 2 de 2). Recuerde que tanto List como ArrayList son tipos genéricos (líneas 12 y 17). El método binarySearch de Collections espera que los elementos de la lista estén en orden ascendente, por lo que la línea 18 en el constructor ordena la lista con el método sort de Collections. Si los elementos de la lista no están ordenados, el resultado es indefinido. En la línea 19 se imprime la lista ordenada en la pantalla. El método buscar (líneas 23 a 31) se llama desde main para realizar las búsquedas. Cada búsqueda llama al método imprimirResultadosBusqueda (líneas 34 a 45) para realizar la búsqueda e imprimir los resultados en pantalla. En la línea 39 se hace una llamada al método binarySearch de Collections para buscar en lista la clave especificada. El método binarySearch recibe un objeto List como primer argumento, y un objeto Object como segundo argumento. En las líneas 41 a 44 se imprimen en pantalla los resultados de la búsqueda. Una versión sobrecargada de binarySearch recibe un objeto Comparator como tercer argumento, el cual especifica la forma en que binarySearch debe comparar los elementos.

19.6.5 Los algoritmos addAll, frequency y disjoint La clase Collections también proporciona los algoritmos addAll, frequency y disjoint. El algoritmo addAll recibe dos argumentos: un objeto Collection en el que se va(n) a insertar el (los) nuevo(s) elemento(s) y un arreglo que proporciona los elementos a insertar. El algoritmo frequency recibe dos argumentos: un objeto Collection en el que se va a buscar y un objeto Object que se va a buscar en la colección. El método frequency devuelve el número de veces que aparece el segundo argumento en la colección. El algoritmo disjoint recibe dos objetos Collections y devuelve true si no tienen elementos en común. En la figura 19.15 se demuestra el uso de los algoritmos addAll, frequency y disjoint. En la línea 19 se inicializa lista con los elementos en el arreglo colores, y en las líneas 20 a 22 se agregan los objetos String "negro", "rojo" y "verde" a vector. En la línea 31 se invoca el método addAll para agregar los elementos en el arreglo colores a vector. En la línea 40 se obtiene la frecuencia del objeto String "rojo" en el objeto Collection llamado vector, usando el método frequency. Observe que en las líneas 41 y 42 se utiliza el nuevo método printf para imprimir la frecuencia en pantalla. En la línea 45 se invoca el método disjoint para evaluar si los objetos Collections lista y vector tienen elementos en común.

www.elsolucionario.net

19.6

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55

Algoritmos de las colecciones

// Fig. 19.15: Algoritmos2.java // Uso de los algoritmos addAll, frequency y disjoint. import java.util.List; import java.util.Vector; import java.util.Arrays; import java.util.Collections; public class Algoritmos2 { private String[] colores = { "rojo", "blanco", "amarillo", "azul" }; private List< String > lista; private Vector< String > vector = new Vector< String >(); // crea objetos List y Vector // y los manipula con métodos de Collections public Algoritmos2() { // inicializa lista y vector lista = Arrays.asList( colores ); vector.add( "negro" ); vector.add( "rojo" ); vector.add( "verde" ); System.out.println( "Antes de addAll, el vector contiene: " ); // muestra los elementos en el vector for ( String s : vector ) System.out.printf( "%s ", s ); // agrega los elementos en colores a lista Collections.addAll( vector, colores ); System.out.println( "\n\nDespues de addAll, el vector contiene: " ); // muestra los elementos en el vector for ( String s : vector ) System.out.printf( "%s ", s ); // obtiene la frecuencia de "rojo" int frecuencia = Collections.frequency( vector, "rojo" ); System.out.printf( "\n\nFrecuencia de rojo en el vector: %d\n", frecuencia ); // comprueba si lista y vector tienen elementos en común boolean desunion = Collections.disjoint( lista, vector ); System.out.printf( "\nlista y vector %s elementos en comun\n", ( desunion ? "no tienen" : "tienen" ) ); } // fin del constructor de Algoritmos2 public static void main( String args[] ) { new Algoritmos2(); } // fin de main } // fin de la clase Algoritmos2

Antes de addAll, el vector contiene: negro rojo verde

Figura 19.15 | Los métodos addAll, frequency y disjoint de Collections. (Parte 1 de 2).

www.elsolucionario.net

819

820

Capítulo 19

Colecciones

Despues de addAll, el vector contiene: negro rojo verde rojo blanco amarillo azul Frecuencia de rojo en el vector: 2 lista y vector tienen elementos en comun

Figura 19.15 | Los métodos addAll, frequency y disjoint de Collections. (Parte 2 de 2).

19.7 La clase Stack del paquete java.util

En el capítulo 17, Estructuras de datos, aprendimos a construir estructuras de datos fundamentales, incluyendo listas enlazadas, pilas, colas y árboles. En un mundo de reutilización de software, en vez de construir las estructuras de datos a medida que las necesitamos, podemos a menudo aprovechar las estructuras de datos existentes. En esta sección, investigaremos la clase Stack en el paquete de utilerías de Java (java.util). En la sección 19.5.3 hablamos sobre la clase Vector, la cual implementa a un arreglo que puede cambiar su tamaño en forma dinámica. La clase Stack extiende a la clase Vector para implementar una estructura de datos tipo pila. La conversión autoboxing ocurre cuando agregamos un tipo primitivo a un objeto Stack, ya que la clase Stack sólo almacena referencias a objetos. En la figura 19.16 se demuestran varios métodos de Stack. Para obtener los detalles de la clase Stack, visite el sitio Web java.sun.com/javase/6/docs/api/java/util/ Stack.html.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

// Fig. 19.16: PruebaStack.java // Programa para probar la clase java.util.Stack. import java.util.Stack; import java.util.EmptyStackException; public class PruebaStack { public PruebaStack() { Stack< Number > pila = new Stack< Number >(); // crea números para almacenarlos en la pila Long numeroLong = 12L; Integer numeroInt = 34567; Float numeroFloat = 1.0F; Double numeroDouble = 1234.5678; // usa el método push pila.push( numeroLong ); // mete un long imprimirPila( pila ); pila.push( numeroInt ); // mete un int imprimirPila( pila ); pila.push( numeroFloat ); // mete un float imprimirPila( pila ); pila.push( numeroDouble ); // mete un double imprimirPila( pila ); // elimina los elementos de la pila try { Number objetoEliminado = null; // saca elementos de la pila while ( true )

Figura 19.16 | La clase Stack del paquete java.util. (Parte 1 de 2).

www.elsolucionario.net

19.7

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67

La clase Stack del paquete java.util

821

{ objetoEliminado = pila.pop(); // usa el método pop System.out.printf( "%s se saco\n", objetoEliminado ); imprimirPila( pila ); } // fin de while } // fin de try catch ( EmptyStackException emptyStackException ) { emptyStackException.printStackTrace(); } // fin de catch } // fin del constructor de PruebaStack private void imprimirPila( Stack< Number > pila ) { if ( pila.isEmpty() ) System.out.print( "la pila esta vacia\n\n" ); // la pila está vacía else // la pila no está vacía { System.out.print( "la pila contiene: " ); // itera a través de los elementos for ( Number numero : pila ) System.out.printf( "%s ", numero ); System.out.print( "(superior) \n\n" ); // indica la parte superior de la pila } // fin de else } // fin del método imprimirPila public static void main( String args[] ) { new PruebaStack(); } // fin de main } // fin de la clase PruebaStack

la pila contiene: 12 (superior) la pila contiene: 12 34567 (superior) la pila contiene: 12 34567 1.0 (superior) la pila contiene: 12 34567 1.0 1234.5678 (superior) 1234.5678 se saco la pila contiene: 12 34567 1.0 (superior) 1.0 se saco la pila contiene: 12 34567 (superior) 34567 se saco la pila contiene: 12 (superior) 12 se saco la pila esta vacia java.util.EmptyStackException at java.util.Stack.peek(Stack.java:85) at java.util.Stack.pop(Stack.java:67) at PruebaStack.(PruebaStack.java:36) at PruebaStack.main(PruebaStack.java:65)

Figura 19.16 | La clase Stack del paquete java.util. (Parte 2 de 2).

www.elsolucionario.net

822

Capítulo 19

Colecciones

En la línea 10 del constructor se crea un objeto Stack vacío de tipo Number. La clase Number (en el paquete es la superclase de la mayoría de las clases de envoltura (como Integer, Double) para los tipos primitivos. Al crear un objeto Stack de objetos Number, se pueden meter en la pila objetos de cualquier clase que extienda a la clase Number. En cada una de las líneas 19, 21, 23 y 25 se hace una llamada al método push de Stack para agregar objetos a la parte superior de la pila. Observe las literales 12L (línea 13) y 1.0F (línea 15). Cualquier literal entera que tenga el sufijo L es un valor long. Cualquier literal entera sin un sufijo es un valor int. De manera similar, cualquier literal de punto flotante que tenga el sufijo F es un valor float. Una literal de punto flotante sin un sufijo es un valor double. Puede aprender más acerca de las literales numéricas en la Especificación del lenguaje Java, en el sitio Web java.sun.com/docs/books/jls/second_edition/html/expressions. doc.html#224125. Un ciclo infinito (líneas 34 a 39) llama al método pop de Stack para eliminar el elemento superior de la pila. El método devuelve una referencia Number al elemento eliminado. Si no hay elementos en el objeto Stack, el método pop lanza una excepción EmptyStackException, la cual termina el ciclo. La clase Stack también declara el método peek. Este método devuelve el elemento superior de la pila sin sacarlo. En la línea 49 se hace una llamada al método isEmpty de Stack (heredado por Stack de la clase Vector) para determinar si la pila está vacía. Si está vacía, el método devuelve true; en caso contrario, devuelve false. El método imprimirPila (líneas 47 a 61) utiliza la instrucción for mejorada para iterar a través de los elementos en la pila. La parte superior actual de la pila (el último valor que se metió a la pila) es el primer valor que se imprime. Como la clase Stack extiende a la clase Vector, toda la interfaz public de la clase Vector está disponible para los clientes de la clase Stack. java.lang)

Tip para prevenir errores 19.1 Como Stack extiende a Vector, todos los métodos public de Vector pueden llamarse en objetos Stack, aún si los métodos no representan operaciones de pila convencionales. Por ejemplo, el método add de Vector se puede utilizar para insertar un elemento en cualquier parte de una pila; una operación que podría “corromper” los datos de la pila. Al manipular un objeto Stack, sólo deben usarse los métodos push y pop para agregar y eliminar elementos de la pila, respectivamente.

19.8 La clase PriorityQueue y la interfaz Queue

En la sección 17.8 presentamos la estructura de datos tipo cola y creamos nuestra propia implementación de ella. En esta sección investigaremos la interfaz Queue y la clase PriorityQueue del paquete de utilerías de Java (java.util). Queue, una nueva interfaz de colecciones introducida en Java SE 5, extiende a la interfaz Collection y proporciona operaciones adicionales para insertar, eliminar e inspeccionar elementos en una cola. PriorityQueue, una de las clases que implementa a la interfaz Queue, ordena los elementos en base a su orden natural, según lo especificado por el método compareTo de los elementos Comparable, o mediante un objeto Comparator que se suministra a través del constructor. La clase PriorityQueue proporciona una funcionalidad que permite inserciones en orden en la estructura de datos subyacente, y eliminaciones de la parte frontal de la estructura de datos subyacente. Al agregar elementos a un objeto PriorityQueue, los elementos se insertan en orden de prioridad, de tal forma que el elemento con mayor prioridad (es decir, el valor más grande) será el primer elemento eliminado del objeto PriorityQueue. Las operaciones comunes de PriorityQueue son: offer para insertar un elemento en la ubicación apropiada, con base en el orden de prioridad, poll para eliminar el elemento de mayor prioridad de la cola de prioridad (es decir, la parte inicial o cabeza de la cola), peek para obtener una referencia al elemento de mayor prioridad de la cola de prioridad (sin eliminar ese elemento), clear para eliminar todos los elementos en la cola de prioridad y size para obtener el número de elementos en la cola de prioridad. En la figura 19.17 se demuestra la clase PriorityQueue. En la línea 10 se crea un objeto PriorityQueue que almacena objetos Double con una capacidad inicial de 11 elementos, y se ordenan los elementos de acuerdo con el ordenamiento natural del objeto (los valores predeterminados para un objeto PriorityQueue). Observe que PriorityQueue es una clase genérica, y que en la línea 10 se crea una instancia de un objeto PriorityQueue con un argumento de tipo Double. La clase PriorityQueue proporciona cinco constructores adicionales. Uno de éstos recibe un int y un objeto Comparator para crear un objeto PriorityQueue con la capacidad inicial especificada por el valor int y el ordenamiento por el objeto Comparator. En las líneas 13 a 15 se utiliza el método offer para agregar elementos a la cola de prioridad.

www.elsolucionario.net

19.9

Conjuntos

823

El método offer lanza una excepción NullPointException si el programa trata de agregar un objeto null a la cola. El ciclo en las líneas 20 a 24 utiliza el método size para determinar si la cola de prioridad está vacía (línea 20). Mientras haya más elementos, en la línea 22 se utiliza el método peek de PriorityQueue para obtener el elemento de mayor prioridad en la cola, para imprimirlo en pantalla (sin eliminarlo de la cola). En la línea 23 se elimina el elemento de mayor prioridad en la cola, con el método poll.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

// Fig. 19.17: PruebaPriorityQueue.java // Programa de prueba de la clase PriorityQueue de la biblioteca estándar. import java.util.PriorityQueue; public class PruebaPriorityQueue { public static void main( String args[] ) { // cola con capacidad de 11 PriorityQueue< Double > cola = new PriorityQueue< Double >(); // inserta elementos en la cola cola.offer( 3.2 ); cola.offer( 9.8 ); cola.offer( 5.4 ); System.out.print( "Sondeando de cola: " ); // muestra los elementos en la cola while ( cola.size() > 0 ) { System.out.printf( "%.1f ", cola.peek() ); // ve el elemento superior cola.poll(); // elimina el elemento superior } // fin de while } // fin de main } // fin de la clase PruebaPriorityQueue

Sondeando de cola: 3.2 5.4 9.8

Figura 19.17 | Programa de prueba de la clase PriorityQueue.

19.9 Conjuntos Un objeto Set es un objeto Collection que contiene elementos únicos (es decir, sin elementos duplicados). El marco de trabajo de colecciones contiene varias implementaciones de Set, incluyendo a HashSet y TreeSet. HashSet almacena sus elementos en una tabla de hash, y TreeSet almacena sus elementos en un árbol. El concepto de las tablas de hash se presenta en la sección 19.10. En la figura 19.18 se utiliza un objeto HashSet para eliminar las cadenas duplicadas de un objeto List. Recuerde que tanto List como Collection son tipos genéricos, por lo que en la línea 18 se crea un objeto List que contiene objetos String, y en la línea 24 se pasa un objeto Collection de objetos String al método imprimirSinDuplicados. El método imprimirSinDuplicados (líneas 24 a 35), el cual es llamado desde el constructor, recibe un argumento Collection. En la línea 27 se crea un objeto HashSet a partir del argumento Collection. Observe que tanto Set como HashSet son tipos genéricos. Por definición, los objetos Set no contienen valores duplicados, por lo que cuando se construye el objeto HashSet, éste elimina cualquier valor duplicado en el objeto Collection. En las líneas 31 y 32 se imprimen en pantalla los elementos en el objeto Set.

Conjuntos ordenados El marco de trabajo de colecciones también incluye la interfaz SortedSet (que extiende a Set) para los conjuntos que mantengan a sus elementos ordenados; ya sea en el orden natural de los elementos (por ejemplo, los

www.elsolucionario.net

824

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

Capítulo 19

Colecciones

// Fig. 19.18: PruebaSet.java // Uso de un objeto HashSet para eliminar duplicados. import java.util.List; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.Collection; public class PruebaSet { private static final String colores[] = { "rojo", "blanco", "azul", "verde", "gris", "naranja", "carne", "blanco", "cyan", "durazno", "gris", "naranja" }; // crea e imprime un objeto ArrayList public PruebaSet() { List< String > lista = Arrays.asList( colores ); System.out.printf( "ArrayList: %s\n", lista ); imprimirSinDuplicados( lista ); } // fin del constructor de PruebaSet // crea conjunto a partir del arreglo para eliminar duplicados private void imprimirSinDuplicados( Collection< String > coleccion ) { // crea un objeto HashSet Set< String > conjunto = new HashSet< String >( coleccion ); System.out.println( "\nLos valores sin duplicados son: " ); for ( String s : conjunto ) System.out.printf( "%s ", s ); System.out.println(); } // fin del método imprimirSinDuplicados public static void main( String args[] ) { new PruebaSet(); } // fin de main } // fin de la clase PruebaSet

ArrayList: [rojo, blanco, azul, verde, gris, naranja, carne, blanco, cyan, durazno, gris, naranja] Los valores sin duplicados son: durazno gris verde azul blanco rojo cyan carne naranja

Figura 19.18 | Objeto HashSet utilizado para eliminar valores duplicados de un arreglo de cadenas. números se encuentran en orden ascendente) o en un orden especificado por un objeto Comparator. La clase TreeSet implementa a SortedSet. El programa de la figura 19.19 coloca cadenas en un objeto TreeSet. Estas cadenas se ordenan al ser agregadas al objeto TreeSet. Este ejemplo también demuestra los métodos de vista de rango, los cuales permiten a un programa ver una porción de una colección. En las líneas 16 y 17 del constructor se crea un objeto TreeSet de objetos String que contiene los elementos del arreglo nombres, y se asigna el objeto SortedSet a la variable arbol. Tanto SortedSet como TreeSet son tipos genéricos. En la línea 20 se imprime en pantalla el conjunto inicial de cadenas, utilizando el método imprimirConjunto (líneas 36 a 42), sobre el cual hablaremos en breve. En la línea 24 se hace una llamada al método headSet de TreeSet para obtener un subconjunto del objeto TreeSet, en el que todos los elementos

www.elsolucionario.net

19.9

Conjuntos

825

serán menores que "naranja". La vista devuelta de headSet se imprime a continuación con imprimirConjunto. Si se hace algún cambio al subconjunto, éste se reflejará también en el objeto TreeSet original, debido a que el subconjunto devuelto por headSet es una vista del objeto TreeSet. En la línea 28 se hace una llamada al método tailSet de TreeSet para obtener un subconjunto en el que cada elemento sea mayor o igual que "naranja", y después se imprime el resultado en pantalla. Cualquier cambio realizado a través de la vista tailSet se realiza también en el objeto TreeSet original. En las líneas 31 y 32 se hace una llamada a los métodos first y last de SortedSet para obtener el elemento más pequeño y más grande del conjunto, respectivamente.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

// Fig. 19.19: PruebaSortedSet.java // Uso de TreeSet y SortedSet. import java.util.Arrays; import java.util.SortedSet; import java.util.TreeSet; public class PruebaSortedSet { private static final String nombres[] = { "amarillo", "verde", "negro", "carne", "gris", "blanco", "naranja", "rojo", "verde" }; // crea un conjunto ordenado con TreeSet, y después lo manipula public PruebaSortedSet() { // crea objeto TreeSet SortedSet< String > arbol = new TreeSet< String >( Arrays.asList( nombres ) ); System.out.println( "conjunto ordenado: " ); imprimirConjunto( arbol ); // imprime el contenido del arbol // obtiene subconjunto mediante headSet, con base en "naranja" System.out.print( "\nheadSet (\"naranja\"): " ); imprimirConjunto( arbol.headSet( "naranja" ) ); // obtiene subconjunto mediante tailSet, con base en "naranja" System.out.print( "tailSet (\"naranja\"): " ); imprimirConjunto( arbol.tailSet( "naranja" ) ); // obtiene los elementos primero y último System.out.printf( "primero: %s\n", arbol.first() ); System.out.printf( "ultimo : %s\n", arbol.last() ); } // fin del constructor de PruebaSortedSet // imprime el conjunto en pantalla private void imprimirConjunto( SortedSet< String > conjunto ) { for ( String s : conjunto ) System.out.print( "%s " , s ); System.out.println(); } // fin del método imprimirConjunto public static void main( String args[] ) { new PruebaSortedSet(); } // fin de main } // fin de la clase PruebaSortedSet

Figura 19.19 | Uso de objetos SortedSet y TreeSet. (Parte 1 de 2).

www.elsolucionario.net

826

Capítulo 19

Colecciones

conjunto ordenado: amarillo blanco carne gris naranja negro rojo verde headSet ("naranja"): tailSet ("naranja"): primero: amarillo ultimo : verde

amarillo blanco carne gris naranja negro rojo verde

Figura 19.19 | Uso de objetos SortedSet y TreeSet. (Parte 2 de 2).

El método imprimirConjunto (líneas 36 a 42) recibe un objeto SortedSet como argumento y lo imprime. En las líneas 38 y 39 imprime en pantalla cada elemento del objeto SortedSet, usando la instrucción for mejorada.

19.10 Mapas Los objetos Map asocian claves a valores y no pueden contener claves duplicadas (es decir, cada clave puede asociarse solamente con un valor; a este tipo de asociación se le conoce como asociación de uno a uno. Los objetos Map difieren de los objetos Set en cuanto a que los primeros contienen claves y valores, mientras que los segundos contienen solamente valores. Tres de las muchas clases que implementan a la interfaz Map son HashTable, HashMap y TreeMap. Los objetos HashTable y HashMap almacenan elementos en tablas de hash, y los objetos TreeMap almacenan elementos en árboles. En esta sección veremos las tablas de hash y proporcionaremos un ejemplo en el que se utiliza un objeto HashMap para almacenar pares clavePvalor. La interfaz SortedMap extiende a Map y mantiene sus claves en orden; ya sea el orden natural de los elementos o un orden especificado por un objeto Comparator. La clase TreeMap implementa a SortedMap.

Implementación de Map con tablas de hash Los lenguajes de programación orientados a objetos facilitan la creación de nuevos tipos. Cuando un programa crea objetos de tipos nuevos o existentes, es probable que necesite almacenarlos y obtenerlos con eficiencia. Los procesos de ordenar y obtener información con los arreglos es eficiente, si cierto aspecto de los datos coincide directamente con un valor de clave numérico, y si las claves son únicas y están estrechamente empaquetadas. Si tenemos 100 empleados con números de seguro social de nueve dígitos, y deseamos almacenar y obtener los datos de los empleados mediante el uso del número de seguro social como una clave, para ello requeriríamos un arreglo con mil millones de elementos, ya que hay mil millones de números únicos de nueve dígitos (000,000,000 a 999,999,999). Esto es impráctico para casi todas las aplicaciones que utilizan números de seguro social como claves. Un programa que tuviera un arreglo de ese tamaño podría lograr un alto rendimiento para almacenar y obtener registros de empleados, con sólo usar el número de seguro social como índice del arreglo. Hay muchas aplicaciones con este problema; a saber, que las claves son del tipo incorrecto (por ejemplo, enteros no positivos que corresponden a los subíndices del arreglo) o que son del tipo correcto, pero se esparcen escasamente sobre un enorme rango. Lo que se necesita es un esquema de alta velocidad para convertir claves, como números de seguro social, números de piezas de inventario y demás, en índices únicos de arreglo. Así, cuando una aplicación necesite almacenar algo, el esquema podría convertir rápidamente la clave de la aplicación en un índice, y el registro podría almacenarse en esa posición en el arreglo. Para obtener datos se hace lo mismo: una vez que la aplicación tenga una clave para la que desee obtener un registro de datos, simplemente aplica la conversión a la clave; esto produce el índice del arreglo en el que se almacenan y obtienen los datos. El esquema que describimos aquí es la base de una técnica conocida como hashing. ¿Por qué ese nombre? Al convertir una clave en un índice de arreglo, literalmente revolvemos los bits, formando un tipo de número “desordenado”. En realidad, el número no tiene un significado real más allá de su utilidad para almacenar y obtener un registro de datos específico. Un fallo en este esquema se denomina colisión; esto ocurre cuando dos claves distintas se asocian a la misma celda (o elemento) en el arreglo. No podemos almacenar dos valores en el mismo espacio, por lo que necesitamos encontrar un hogar alterno para todos los valores más allá del primero, que se asocie con un índice de arreglo específico. Hay muchos esquemas para hacer esto. Uno de ellos es “hacer hash de nuevo” (es decir, aplicar otra transformación de hashing a la clave, para proporcionar la siguiente celda como candidato en el arreglo). El pro-

www.elsolucionario.net

19.10

Mapas

827

ceso de hashing está diseñado para distribuir los valores en toda la tabla, por lo que se asume que se encontrará una celda disponible con sólo unas cuantas transformaciones de hashing. Otro esquema utiliza un hash para localizar la primera celda candidata. Si esa celda está ocupada, se buscan celdas sucesivas en orden, hasta que se encuentra una disponible. El proceso de obtención funciona de la misma forma: se aplica hash a la clave una vez para determinar la función inicial y comprobar si contiene los datos deseados. Si es así, la búsqueda termina. En caso contrario, se busca linealmente en las celdas sucesivas hasta encontrar los datos deseados. La solución más popular a las colisiones en las tablas de hash es hacer que cada celda de la tabla sea una “cubeta” de hash que, por lo general, viene siendo una lista enlazada de todos los pares clave/valor que se asocian con esa celda. Ésta es la solución que implementan las clases Hashtable y HashMap (del paquete java.util). Tanto Hashtable como HashMap implementan a la interfaz Map. Las principales diferencias entre ellas son que HashMap no está sincronizada (varios subprocesos no deben modificar un objeto HashMap en forma concurrente), y permite claves y valores null. El factor de carga de una tabla de hash afecta al rendimiento de los esquemas de hashing. El factor de carga es la proporción del número de celdas ocupadas en la tabla de hash, con respecto al número total de celdas en la tabla de hash. Entre más se acerque esta proporción a 1.0, mayor será la probabilidad de colisiones.

Tip de rendimiento 19.7 El factor de carga en una tabla de hash es un clásico ejemplo de una concesión entre espacio de memoria y tiempo de ejecución: al incrementar el factor de carga, obtenemos un mejor uso de la memoria, pero el programa se ejecuta con más lentitud, debido al incremento en las colisiones de hashing. Al reducir el factor de carga, obtenemos más velocidad en la ejecución del programa, debido a la reducción en las colisiones de hashing, pero obtenemos un uso más pobre de la memoria, debido a que una proporción más grande de la tabla de hash permanece vacía.

Las tablas de hash son complejas de programar. Los estudiantes de ciencias computacionales estudian los esquemas de hashing en cursos titulados “Estructuras de datos” y “Algoritmos”. Java proporciona las clases Hashtable y HashMap para permitir a los programadores utilizar la técnica de hashing sin tener que implementar los mecanismos de las tablas de hash. Este concepto es muy importante en nuestro estudio de la programación orientada a objetos. Como vimos en capítulos anteriores, las clases encapsulan y ocultan la complejidad (es decir, los detalles de implementación) y ofrecen interfaces amigables para el usuario. La fabricación apropiada de clases para exhibir tal comportamiento es una de las habilidades más valiosas en el campo de la programación orientada a objetos. En la figura 19.20 se utiliza un objeto HashMap para contar el número de ocurrencias de cada palabra en una cadena. En la línea 17 se crea un objeto HashMap vacío con una capacidad inicial predeterminada (16 elementos) y un factor de carga predeterminado (0.75); estos valores predeterminados están integrados en la implementación de HashMap. Cuando el número de posiciones ocupadas en el objeto HashMap se vuelve mayor que la capacidad multiplicada por el factor de carga, la capacidad se duplica en forma automática. Observe que HashMap es una clase genérica que recibe dos argumentos de tipo. El primero especifica el tipo de clave (es decir, String) y el segundo el tipo de valor (es decir, Integer). Recuerde que los argumentos de tipo que se pasan a una clase gené-

1 2 3 4 5 6 7 8 9 10 11 12

// Fig. 19.20: ConteoTipoPalabras.java // Programa que cuenta el número de ocurrencias de cada palabra en una cadena import java.util.StringTokenizer; import java.util.Map; import java.util.HashMap; import java.util.Set; import java.util.TreeSet; import java.util.Scanner; public class ConteoTipoPalabras { private Map< String, Integer > mapa;

Figura 19.20 | Objetos Hashmap y Map. (Parte 1 de 3).

www.elsolucionario.net

828

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70

Capítulo 19

Colecciones

private Scanner scanner; public ConteoTipoPalabras() { mapa = new HashMap< String, Integer >(); // crea objeto HashMap scanner = new Scanner( System.in ); // crea objeto scanner crearMap(); // crea un mapa con base en la entrada del usuario mostrarMap(); // muestra el contenido del mapa } // fin del constructor de ConteoTipoPalabras // crea mapa a partir de la entrada del usuario private void crearMap() { System.out.println( "Escriba una cadena:" ); // pide la entrada del usuario String entrada = scanner.nextLine(); // crea objeto StringTokenizer para los datos de entrada StringTokenizer tokenizer = new StringTokenizer( entrada ); // procesamiento del texto de entrada while ( tokenizer.hasMoreTokens() ) // mientras haya más entrada { String palabra = tokenizer.nextToken().toLowerCase(); // obtiene una palabra // si el mapa contiene la palabra if ( mapa.containsKey( palabra ) ) // está la palabra en el mapa? { int cuenta = mapa.get( palabra ); // obtiene la cuenta actual mapa.put( palabra, cuenta + 1 ); // incrementa la cuenta } // fin de if else mapa.put( palabra, 1 ); // agrega una nueva palabra con una cuenta de 1 al mapa } // fin de while } // fin del método crearMap // muestra el contenido del mapa private void mostrarMap() { Set< String > claves = mapa.keySet(); // obtiene las claves // ordena las claves TreeSet< String > clavesOrdenadas = new TreeSet< String >( claves ); System.out.println( "El mapa contiene:\nClave\t\tValor" ); // genera la salida para cada clave en el mapa for ( String clave : clavesOrdenadas ) System.out.printf( "%-10s%10s\n", clave, mapa.get( clave ) ); System.out.printf( "\nsize:%d\nisEmpty:%b\n", mapa.size(), mapa.isEmpty() ); } // fin del método mostrarMap public static void main( String args[] ) { new ConteoTipoPalabras(); } // fin de main } // fin de la clase ConteoTipoPalabras

Figura 19.20 | Objetos Hashmap y Map. (Parte 2 de 3).

www.elsolucionario.net

19.11

La clase Properties

829

Escriba una cadena: Ser o no ser; esa es la pregunta Si es mas noble sufrir El mapa contiene: Clave Valor es 2 esa 1 la 1 mas 1 no 1 noble 1 o 1 pregunta 1 ser 1 ser; 1 si 1 sufrir 1 size:12 isEmpty:false

Figura 19.20 | Objetos Hashmap y Map. (Parte 3 de 3). rica deben ser tipos de referencias, por lo cual el segundo argumento de tipo es Integer, no int. En la línea 18 se crea un objeto Scanner que lee la entrada del usuario del flujo estándar de entrada. En la línea 19 se hace una llamada al método crearMap (líneas 24 a 46), el cual usa un mapa para almacenar el número de ocurrencias de cada palabra en la oración. En la línea 27 se invoca el método nextLine de scanner para obtener la entrada del usuario, y en la línea 30 se crea un objeto StringTokenizer para descomponer la cadena de entrada en sus palabras componentes individuales. Este constructor de StringTokenizer recibe un argumento de cadena y crea un objeto StringTokenizer para esa cadena, y utilizará el espacio en blanco para separarla. La condición en la instrucción while de las líneas 33 a 45 utiliza el método hasMoreTokens de StringTokenizer para determinar si hay más tokens en la cadena que se está separando en tokens. Si es así, en la línea 35 se convierte el siguiente token a minúsculas. El siguiente token se obtiene mediante una llamada al método nextToken de StringTokenizer, el cual devuelve un objeto String. [Nota: en la sección 30.6 hablaremos sobre la clase StringTokenizer con detalle]. Después, en la línea 38 se hace una llamada al método containsKey de Mapa para determinar si la palabra está en el mapa (y por ende, ha ocurrido antes en la cadena). Si el objeto Mapa no contiene una asignación para la palabra, en la línea 44 se utiliza el método put de Mapa para crear una nueva entrada en el mapa, con la palabra como la clave y un objeto Integer que contiene 1 como valor. Observe que la conversión autoboxing ocurre cuando el programa pasa el entero 1 al método put, ya que el mapa almacena el número de ocurrencias de la palabra como un objeto Integer. Si la palabra no existe en el mapa, en la línea 40 se utiliza el método get de Mapa para obtener el valor asociado de la clave (la cuenta) en el mapa. En la línea 41 se incrementa ese valor y se utiliza put para reemplazar el valor asociado de la clave en el mapa. El método put devuelve el valor anterior asociado con la clave, o null si la clave no estaba en el mapa. El método mostrarMap (líneas 49 a 64) muestra todas las entradas en el mapa. Utiliza el método keySet (línea 51) de HashMap para obtener un conjunto de las claves. Estas claves tienen el tipo String en el mapa, por lo que el método keySet devuelve un tipo genérico Set con el parámetro de tipo especificado como String. En la línea 54 se crea un objeto TreeSet de las claves, en el cual se ordenan éstas. El ciclo en las líneas 59 a 60 accede a cada clave y a su valor en el mapa. En la línea 60 se muestra cada clave y su valor, usando el especificador de formato %-10s para justificar cada clave a la izquierda, y el especificador de formato %10s para justificar cada valor a la derecha. Observe que las claves se muestran en orden ascendente. En la línea 63 se hace una llamada al método size de Mapa para obtener el número de pares clave-valor en el objeto Map. En la línea 63 se hace una llamada a isEmpty, el cual devuelve un valor boolean que indica si el objeto Map está vacío o no.

19.11 La clase Properties

Un objeto Properties es un objeto Hashtable persistente que, por lo general, almacena pares clave-valor de cadenas; suponiendo que el programador utiliza los métodos setProperty y getProperty para manipular la

www.elsolucionario.net

830

Capítulo 19

Colecciones

tabla, en vez de los métodos put y get heredados de Hashtable. Al decir “persistente”, significa que el objeto Properties se puede escribir en un flujo de salida (posiblemente un archivo) y se puede leer de vuelta, a través de un flujo de entrada. Un uso común de los objetos Properties en versiones anteriores de Java era mantener los datos de configuración de una aplicación, o las preferencias del usuario para las aplicaciones. [Nota: la API Preferences (paquete java.util.prefs) está diseñada para reemplazar este uso específico de la clase Properties, pero esto se encuentra más allá del alcance de este libro. Para aprender más, visite el sitio Web java.sun. com/javase/6/docs/technotes/guides/preferentes/index.html]. La clase Properties extiende a la clase Hashtable. En la figura 19.21 se demuestran varios métodos de la clase Properties.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

// Fig. 19.21: PruebaProperties.java // Demuestra la clase Properties del paquete java.util. import java.io.FileOutputStream; import java.io.FileInputStream; import java.io.IOException; import java.util.Properties; import java.util.Set; public class PruebaProperties { private Properties tabla; // establece la GUI para probar la tabla Properties public PruebaProperties() { tabla = new Properties(); // crea la tabla Properties // establece las propiedades tabla.setProperty( "color", "azul" ); tabla.setProperty( "anchura", "200" ); System.out.println( "Despues de establecer propiedades" ); listarPropiedades(); // muestra los valores de las propiedades // reemplaza el valor de una propiedad tabla.setProperty( "color", "rojo" ); System.out.println( "Despues de reemplazar propiedades" ); listarPropiedades(); // muestra los valores de las propiedades guardarPropiedades(); // guarda las propiedades tabla.clear(); // vacia la tabla System.out.println( "Despues de borrar propiedades" ); listarPropiedades(); // muestra los valores de las propiedades cargarPropiedades(); // carga las propiedades // obtiene el valor de la propiedad color Object valor = tabla.getProperty( "color" ); // comprueba si el valor está en la tabla if ( valor != null ) System.out.printf( "El valor de la propiedad color es %s\n", valor ); else System.out.println( "La propiedad color no está en la tabla" );

Figura 19.21 | La clase Properties del paquete java.util. (Parte 1 de 3).

www.elsolucionario.net

19.11

48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105

La clase Properties

831

} // fin del constructor de PruebaProperties // guarda las propiedades en un archivo public void guardarPropiedades() { // guarda el contenido de la tabla try { FileOutputStream salida = new FileOutputStream( "props.dat" ); tabla.store( salida, "Propiedades de ejemplo" ); // guarda las propiedades salida.close(); System.out.println( "Despues de guardar las propiedades" ); listarPropiedades(); // muestra los valores de las propiedades } // fin de try catch ( IOException ioException ) { ioException.printStackTrace(); } // fin de catch } // fin del método guardarPropiedades // carga las propiedades de un archivo public void cargarPropiedades() { // carga el contenido de la tabla try { FileInputStream entrada = new FileInputStream( "props.dat" ); tabla.load( entrada ); // carga las propiedades entrada.close(); System.out.println( "Después de cargar las propiedades" ); listarPropiedades(); // muestra los valores de las propiedades } // fin de try catch ( IOException ioException ) { ioException.printStackTrace(); } // fin de catch } // fin del método cargarPropiedades // imprime los valores de las propiedades public void listarPropiedades() { Set< Object > claves = tabla.keySet(); // obtiene los nombres de las propiedades // imprime los pares nombre/valor for ( Object clave : claves ) { System.out.printf( "%s\t%s\n", clave, tabla.getProperty( ( String ) clave ) ); } // fin de for System.out.println(); } // fin del método listarPropiedades public static void main( String args[] ) { new PruebaProperties(); } // fin de main } // fin de la clase PruebaProperties

Figura 19.21 | La clase Properties del paquete java.util. (Parte 2 de 3).

www.elsolucionario.net

832

Capítulo 19

Colecciones

Despues de establecer propiedades anchura 200 color azul Despues de reemplazar propiedades anchura 200 color rojo Despues de guardar las propiedades anchura 200 color rojo Despues de borrar propiedades Después de cargar las propiedades anchura 200 color rojo El valor de la propiedad color es rojo

Figura 19.21 | La clase Properties del paquete java.util. (Parte 3 de 3). En la línea 16 se utiliza el constructor sin argumentos para crear un objeto Properties llamado tabla sin propiedades predeterminadas. La clase Properties también cuenta con un constructor sobrecargado, el cual recibe una referencia a un objeto Properties que contiene valores de propiedad predeterminados. En cada una de las líneas 19 y 20 se hace una llamada al método setProperty de Properties para almacenar un valor para la clave especificada. Si la clave no existe en la tabla, setProperty devuelve null; en caso contrario, devuelve el valor anterior para esa clave. En la línea 41 se llama al método getProperty de Properties para localizar el valor asociado con la clave especificada. Si la clave no se encuentra en este objeto Properties, getProperty devuelve null. Una versión sobrecargada de este método recibe un segundo argumento, el cual especifica el valor predeterminado a devolver si getProperty no puede localizar la clave. En la línea 57 se hace una llamada al método store de Properties para guardar el contenido del objeto Properties en el objeto OutputStream especificado como el primer argumento (en este caso, el objeto salida de FileOutputStream). El segundo argumento, un objeto String, es una descripción del objeto Properties. La clase Properties también proporciona el método list, el cual recibe un argumento PrintStream. Este método es útil para mostrar la lista de propiedades. En la línea 75 se hace una llamada al método load de Properties para restaurar el contenido del objeto Properties a partir del objeto InputStream especificado como el primer argumento (en este caso, un objeto FileInputStream). En la línea 89 se hace una llamada al método keySet de Properties para obtener un objeto Set de los nombres de las propiedades. En la línea 94 se obtiene el valor de una propiedad, para lo cual se pasa una clave al método getProperty.

19.12 Colecciones sincronizadas En el capítulo 23 hablaremos sobre el subprocesamiento múltiple. Con la excepción de Vector y Hashtable, las colecciones en el marco de trabajo de colecciones están desincronizadas de manera predeterminada, por lo que pueden operar eficientemente cuando no se requiere el subprocesamiento múltiple. Sin embargo, debido a que están desincronizadas, el acceso concurrente a un objeto Collection por parte de varios subprocesos podría producir resultados indeterminados, o errores fatales. Para evitar potenciales problemas de subprocesamiento, se utilizan envolturas de sincronización para las colecciones que podrían ser utilizadas por varios subprocesos. Un objeto envoltura recibe llamadas a métodos, agrega la sincronización de subprocesos (para evitar un acceso concurrente a la colección) y delega las llamadas al objeto de la colección envuelto. La API Collections proporciona un conjunto de métodos static para envolver colecciones como versiones sincronizadas. En la figura 19.22 se enlistan los encabezados para las envolturas de sincronización. Los detalles acerca de estos métodos están dispo-

www.elsolucionario.net

19.13

Colecciones no modificables

833

nibles en java.sun.com/javase/6/docs/api/java/util/Collections.html. Todos estos métodos reciben un tipo genérico como parámetro y devuelven una vista sincronizada del tipo genérico. Por ejemplo, el siguiente código crea un objeto List sincronizado (lista2) que almacena objetos String: List< String > lista1 = new ArrayList< String >(); List< String > lista2 = Collections.synchronizedList( lista1 );

Encabezados de los métodos public

static

< T > Collection< T > synchronizedCollection( Collection< T > c ) < T > List< T > synchronizedList( List< T > unaLista ) < T > Set< T > synchronizedSet( Set< T > s ) < T > SortedSet< T > synchronizedSortedSet( SortedSet< T > s ) < K, V > Map< K, V > synchronizedMap( Map< K, V > m ) < K, V > SortedMap< K, V > synchronizedSortedMap( SortedMap< K, V > m )

Figura 19.22 | Métodos de envoltura de sincronización.

19.13 Colecciones no modificables La API Collections proporciona un conjunto de métodos static que crean envolturas no modificables para las colecciones. Las envolturas no modificables lanzan excepciones UnsupportedOperationException si se producen intentos por modificar la colección. En la figura 19.23 se enlistan los encabezados para estos métodos. Los detalles acerca de estos métodos están disponibles en java.sun.com/javase/6/docs/api/java/util/ Collections.html. Todos estos métodos reciben un tipo genérico como parámetro y devuelven una vista no modificable del tipo genérico. Por ejemplo, el siguiente código crea un objeto List no modificable (lista2) que almacena objetos String: List< String > lista1 = new ArrayList< String >(); List< String > lista2 = Collections.unmodifiableList( lista1 );

Observación de ingeniería de software 19.5 Puede utilizar una envoltura no modificable para crear una colección que ofrezca acceso de sólo lectura a otros, mientras que a usted le permita acceso de lectura/escritura. Para ello, simplemente dé a los otros una referencia a la envoltura no modificable, y usted conserve una referencia a la colección original.

Encabezados de los métodos public

static

< T > Collection< T > unmodifiableCollection( Collection< T > c ) < T > List< T > unmodifiableList( List< T > unaLista ) < T > Set< T > unmodifiableSet( Set< T > s ) < T > SortedSet< T > unmodifiableSortedSet( SortedSet< T > s ) < K, V > Map< K, V > unmodifiableMap( Map< K, V > m ) < K, V > SortedMap< K, V > unmodifiableSortedMap( SortedMap< K, V > m )

Figura 19.23 | Métodos de envolturas no modificables.

www.elsolucionario.net

834

Capítulo 19

Colecciones

19.14 Implementaciones abstractas El marco de trabajo de colecciones proporciona varias implementaciones abstractas de interfaces de Collection, a partir de las cuales el programador puede construir implementaciones completas. Estas implementaciones abstractas incluyen una implementación de Collection delgada, llamada AbstractCollection; una implementación de List delgada, la cual permite el acceso aleatorio a sus elementos y se le conoce como AbstractList; una implementación de Map delgada conocida como AbstractMap, una implementación de List delgada que permite un acceso secuencial a sus elementos y se le conoce como AbstractSequentialList, una implementación de Set delgada, conocida como AbstractSet; y una implementación de Queue delgada, conocida como AbstractQueue. Puede aprender más acerca de estas clases en java.sun.com/javase/6/docs/api/ java/util/package-summary.html. Para escribir una implementación personalizada, puede extender la implementación abstracta que se adapte mejor a sus necesidades, e implementar cada uno de los métodos abstract de la clase. Después, si su colección es modificable, sobrescriba cualquier método concreto que evite su modificación.

19.15 Conclusión En este capítulo se presentó el marco de tabajo de colecciones de Java. Aprendió a utilizar la clase Arrays para realizar manipulaciones con arreglos. Conoció la jerarquía de colecciones y aprendió a utilizar las interfaces del marco de trabajo de colecciones para programar con las colecciones mediante el polimorfismo. También conoció varios algoritmos predefinidos para manipular colecciones. En el siguiente capítulo presentaremos los applets de Java, los cuales son programas en Java que, por lo general, se ejecutan en un explorador Web. Empezaremos con applets de ejemplo que vienen con el JDK, y después le mostraremos cómo escribir y ejecutar sus propios applets.

Resumen Sección 19.1 Introducción • El marco de trabajo de colecciones de Java proporciona acceso al programador las estructuras de datos preempaquetadas, así como a los algoritmos para manipularlas.

Sección 19.2 Generalidades acerca de las colecciones • Una colección es un objeto que puede contener referencias a otros objetos. Las interfaces de colecciones declaran las operaciones que pueden realizarse en cada tipo de colección. • Las clases y las interfaces del marco de trabajo de colecciones se encuentran en el paquete java.util.

Sección 19.3 La clase Arrays • La clase Arrays proporciona métodos static para manipular arreglos, incluyendo a sort para ordenar un arreglo, a binarySearch para buscar en un arreglo ordenado, a equals para comparar arreglos y a fill para colocar elementos en un arreglo. • El método asList de Arrays devuelve una vista List de un arreglo, la cual permite a un programa manipular el arreglo como si fuera un objeto List. Cualquier modificación realizada a través de la vista List modifica el arreglo, y cualquier modificación al arreglo modifica a la vista List. • El método size obtiene el número de elementos en un objeto List, y el método get devuelve un elemento del objeto List.

Sección 19.4 La interfaz Collection y la clase Collections • La interfaz Collection es la interfaz raíz en la jerarquía de colecciones, a partir de la cual se derivan las interfaces Set y List. La interfaz Collection contiene operaciones masivas para agregar, borrar, comparar y retener objetos en una colección. La interfaz Collection proporciona un método llamado iterator para obtener un objeto Iterator. • La clase Collections proporciona métodos static para manipular colecciones. Muchos de los métodos son implementaciones de algoritmos polimórficos para buscar, ordenar, etcétera.

www.elsolucionario.net

Resumen

835

Sección 19.5 Listas • Un objeto List es un objeto Collection ordenado, que puede contener elementos duplicados. • La interfaz List se implementa mediante las clases ArrayList, LinkedList y Vector. La clase ArrayList es una implementación tipo arreglo de un objeto List, que puede cambiar su tamaño. Un objeto LinkedList es una implementación tipo lista enlazada de un objeto List. • El método hasNext de Iterator determina si un objeto Collection contiene otro elemento. El método next devuelve una referencia al siguiente objeto en el objeto Collection, y avanza el objeto Iterator. • El método subList devuelve una vista de una porción de un objeto List. Cualquier modificación realizada en esta vista se realiza también en el objeto List. • El método clear elimina elementos de un objeto List. • El método toArray devuelve el contenido de una colección, en forma de un arreglo. • La clase Vector maneja arreglos que pueden cambiar su tamaño en forma dinámica. En cualquier momento dado, un objeto Vector contiene un número de elementos menor o igual a su capacidad. Si un objeto Vector necesita crecer, aumenta en base a su incremento de capacidad. Si no se especifica un incremento de capacidad, Java duplica el tamaño del objeto Vector cada vez que se requiere una capacidad adicional. La capacidad predeterminada es de 10 elementos. • El método add de Vector agrega su argumento al final del objeto Vector. El método insertElementAt inserta un elemento en la posición especificada. El método set establece el elemento en una posición específica. • El método remove de Vector elimina del objeto Vector la primera ocurrencia de su argumento. El método removeAllElements elimina todos los elementos del objeto Vector El método removeElementAt elimina el elemento en el índice especificado. • El método firstElement de Vector devuelve una referencia al primer elemento. El método lastElement devuelve una referencia al último elemento. • El método contains de Vector determina si el objeto Vector contiene la claveBusqueda especificada como argumento. El método indexOf de Vector obtiene el índice de la primera ubicación de su argumento. El método devuelve -1 si el argumento no se encuentra en el objeto Vector. • El método isEmpty de Vector determina si el objeto Vector está vacío. Los métodos size y capacity determinan el número de elementos actuales en el objeto Vector, y el número de elementos que pueden almacenarse en el objeto Vector sin asignar más memoria, respectivamente.

Sección 19.6 Algoritmos de colecciones • Los algoritmos sort, binarySearch, reverse, shuffle, fill y copy operan en objetos List. Los algoritmos min y max operan en objetos Collection. El algoritmo reverse invierte los elementos de un objeto List, el algoritmo fill establece cada elemento del objeto List a un objeto Object especificado, y copy copia elementos de un objeto List a otro objeto List. El algoritmo sort ordena los elementos de un objeto List. • El algoritmo addAll anexa a una colección todos los elementos en un arreglo, el algoritmo frequency calcula cuántos elementos en la colección son iguales al elemento especificado, y disjoint determina si dos colecciones tienen elementos en común. • Los algoritmos min y max buscan los elementos mayor y menor en una colección. • La interfaz Comparator proporciona un medio para ordenar los elementos de un objeto Collection en un orden distinto a su orden natural. • El método reverseOrder de Collections devuelve un objeto Comparator que puede usarse con sort para ordenar elementos de una colección en forma inversa. • El algoritmo shuffle ordena al azar los elementos de un objeto List. • El algoritmo binarySearch localiza un objeto Object en un objeto List ordenado.

Sección 19.7 La clase Stack del paquete java.util • La clase Stack extiende a Vector. El método push de Stack agrega su argumento a la parte superior de la pila. El método pop elimina el elemento superior de la pila. El método peek devuelve una referencia al elemento superior sin eliminarlo. El método empty de Stack determina si la pila está vacía o no.

Sección 19.8 La clase PriorityQueue y la interfaz Queue •

Queue, una nueva interfaz de colecciones presentada en Java SE 5, extiende a la interfaz Collection y proporciona operaciones adicionales para insertar, eliminar e inspeccionar elementos en una cola. • PriorityQueue, una de las implementaciones de Queue, ordena los elementos en base a su orden natural (es decir, la implementación del método compareTo) o mediante un objeto Comparator que se suministra a través del constructor.

www.elsolucionario.net

836

Capítulo 19

Colecciones

• Las operaciones comunes de PriorityQueue son: offer para insertar un elemento en la ubicación apropiada, con base en el orden de prioridad; poll para eliminar el elemento de mayor prioridad de la cola de prioridad (es decir, la parte inicial o cabeza de la cola); peek para obtener una referencia al elemento de mayor prioridad de la cola de prioridad; clear para eliminar todos los elementos de la cola de prioridad; y size para obtener el número de elementos en la cola de prioridad.

Sección 19.9 Conjuntos • Un objeto Set es un objeto Collection que no contiene elementos duplicados. HashSet almacena sus elementos en una tabla de hash. TreeSet almacena sus elementos en un árbol. • La interfaz SortedSet extiende a Set y representa un conjunto que mantiene sus elementos ordenados. La clase TreeSet implementa a SortedSet. • El método headSet de TreeSet obtiene una vista de un objeto TreeSet que es menor a un elemento especificado. El método tailSet obtiene una vista que es mayor o igual a un elemento especificado. Cualquier modificación realizada a la vista se realiza al objeto TreeSet.

Sección 19.10 Mapas • Los objetos Map asocian claves con valores y no pueden contener claves duplicadas. Los objetos Map difieren de los objetos Set en cuanto a que los objetos Map contienen tanto claves como valores, mientras que los objetos Set sólo contienen valores. Los objetos HashMap almacenan elementos en una tabla de hash, y los objetos TreeMap almacenan elementos en un árbol. • Los objetos Hashtable y HashMap almacenan elementos en tablas de hash, y los objetos TreeMap almacenan elementos en árboles. • HashMap es una clase genérica que recibe dos argumentos de tipo. El primer argumento de tipo especifica el tipo de la clave, y el segundo especifica el tipo de valor. • El método put de HashMap agrega una clave y un valor en un objeto HashMap. El método get localiza el valor asociado con la clave especificada. El método isEmpty determina si el mapa está vacío. • El método keySet de HashMap devuelve un conjunto de las claves. Los métodos size e isEmpty de map devuelven el número de pares clave-valor en el objeto Map, y un valor booleano que indica si el objeto Map está vacío, respectivamente. • La interfaz SortedMap extiende a Map y representa un mapa que mantiene sus claves en orden. La clase TreeMap implementa a SortedMap.

Sección 19.11 La clase Properties • Un objeto Properties es un objeto Hashtable persistente. La clase Properties extiende a Hashtable. • El constructor de Properties sin argumentos crea una tabla Properties vacía sin propiedades predeterminadas. También hay un constructor sobrecargado que recibe una referencia a un objeto Properties predeterminado que contiene valores de propiedades predeterminados. • El método setProperty de Properties especifica el valor asociado con el argumento tipo clave. El método getProperty de Properties localiza el valor de la clave especificada como argumento. El método store guarda el contenido del objeto Properties en el objeto OutputStream especificado como el primer argumento. El método load restaura el contenido del objeto Properties del objeto InputStream que se especifica como el argumento.

Sección 19.12 Colecciones sincronizadas • Las colecciones del marco de trabajo de colecciones están desincronizadas. Las envolturas de sincronización se proporcionan para las colecciones a las que pueden acceder varios subprocesos en forma simultánea.

Sección 19.13 Colecciones no modificables • La API Collections proporciona un conjunto de métodos public static para convertir colecciones en versiones no modificables. Las envolturas no modificables lanzan excepciones UnsupportedOperationException si hay intentos de modificar la colección.

Sección 19.14 Implementaciones abstractas • El marco de trabajo de colecciones proporciona varias implementaciones abstractas de las interfaces de colecciones, a partir de las cuales el programador puede crear rápidamente implementaciones personalizadas completas.

www.elsolucionario.net

Terminología

Terminología AbstractCollection, clase AbstractList, clase AbstractMap, clase AbstractQueue, clase AbstractSequentialList, clase AbstractSet, clase add, método de List add, método de Vector addAll, método de Collections addFirst, método de List addLast, método de List algoritmos en Collections ArrayList

Hashtable, clase hasMoreTokens, método de StringTokenizer hasNext, método de Iterator hasPrevious, método de ListIterator incremento de capacidad de un objeto Vector indexOf, método de Vector

arreglo arreglos como colecciones asignación de uno a uno asignaciones asList, método de Arrays asociar claves con valores binarySearch, método de Arrays binarySearch, método de Collections capacity, método de Vector clase de envoltura clave en HashMap clear, método de List clear, método de PriorityQueue colección ordenada colecciones colocadas en arreglos colecciones modificables colecciones no modificables colisión en hashing collection Collection, interfaz Collections, clase Comparable, interfaz comparación lexicográfica Comparator, interfaz compareTo, método de Comparable contains, método de vector containsKey, método de HashMap copy, método de Collections disjoint, método de Collections elementos duplicados eliminar un elemento de una colección envolturas de sincronización factor de carga en hashing fill, método de Arrays fill, método de Collections firstElement, método de Vector frequency, método de Collections get, método de HashMap getProperty, método de la clase Properties hashing HashMap, clase HashSet, clase

insertar un elemento en una colección isEmpty, método de Map isEmpty, método de Vector iterador iterador bidireccional iterar a través de los elementos de un contenedor Iterator, interfaz keySet, método de HashMap lastElement, método de Vector LinkedList, clase List, interfaz ListIterator, interfaz Map, interfaz de colección mapa marco de trabajo de colecciones max, método de Collections método de comparación natural métodos de vista de rango min, método de Collections next, método de Iterator nextToken, método de StringTokenizer NoSuchElementException, clase offer, método de PriorityQueue ordenamiento ordenamiento estable ordenamiento natural ordenar un objeto List par clave/valor peek, método de PriorityQueue peek, método de Stack poll, método de PriorityQueue pop, método de Stack PriorityQueue, clase Properties, clase put, método de HashMap Queue, interfaz removeAllElements, método de Vector removeElement, método de Vector removeElementAt, método de Vector reverse, método de Collections reverseOrder, método de Collections secuencia Set, interfaz shuffle, método de Collections size, método de List size, método de PriorityQueue sort, método de Arrays sort, método de Collections SortedMap, interfaz de colección

www.elsolucionario.net

837

838

Capítulo 19

Colecciones

SortedSet, interfaz de colección Stack, clase StringTokenizer, clase TreeMap, clase

TreeSet, clase ver un arreglo como un objeto List vista

Ejercicios de autoevaluación 19.1

Complete las siguientes oraciones: a) Un(a) _________________ se utiliza para recorrer una colección y puede eliminar elementos de la colección, durante la iteración. b) Para acceder a un elemento en un objeto List, se utiliza el _________________ del elemento. c) A los objetos List se les conoce algunas veces como _________________. d) Las clases _________________ y _________________ de Java proporcionan las herramientas de estructuras de datos tipo arreglo, que pueden cambiar su tamaño en forma dinámica. e) Si usted no especifica un incremento de capacidad, el sistema _________________ el tamaño del objeto Vector cada vez que se requiere una capacidad adicional. f ) Puede utilizar un(a) _________________ para crear una colección que ofrezca acceso de sólo lectura a los demás, mientras que a usted le permita el acceso de lectura/escritura. g) Los objetos _________________ se pueden utilizar para crear pilas, colas, árboles y deques (colas con doble extremo). h) El algoritmo _________________ de Collections determina si dos colecciones tienen elementos en común.

19.2

Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué. a) Los valores de tipos primitivos pueden almacenarse directamente en un objeto Vector. b) Un objeto Set puede contener valores duplicados. c) Un objeto Map puede contener claves duplicadas. d) Un objeto LinkedList puede contener valores duplicados. e) Collections es una interfaz (interface). f ) Los objetos Iterator pueden eliminar elementos. g) Con la técnica de hashing, a medida que se incrementa el factor de carga, disminuye la probabilidad de colisiones. h) Un objeto PriorityQueue permite elementos null.

Respuestas a los ejercicios de autoevaluación 19.1 a) Iterator. b) índice. c) secuencias. d) ArrayList, Vector. e) duplicará. f ) no modificable wrapper. g) LinkedLists. h) disjoint. 19.2 a) Falso; un objeto Vector sólo almacena objetos. La conversión autoboxing ocurre cuando se agrega un tipo primitivo al objeto Vector, lo cual significa que el tipo primitivo se convierte en su clase de envoltura de tipo correspondiente. b) Falso. Un objeto Set no puede contener valores duplicados. c) Falso. Un objeto Map no puede contener claves duplicadas. d) Verdadero. e) Falso. Collections es una clase; Collection es una interfaz (interface). f ) Verdadero. g) Falso. Con la técnica de hashing, a medida que aumenta el factor de carga, hay menos posiciones disponibles, relativas al número total de posiciones, por lo que la probabilidad de seleccionar una posición ocupada (una colisión) con una operación de hashing se incrementa. h) Falso. Una excepción NullPointerException se lanza si el programa trata de agregar null a un objeto PriorityQueue.

Ejercicios 19.3

Defina cada uno de los siguientes términos: a) Collection b) Collections

www.elsolucionario.net

Ejercicios

839

c) Comparator d) List e) factor de carga f ) colisión g) concesión entre espacio y tiempo en hashing h) HashMap 19.4 Explique brevemente la operación de cada uno de los siguientes métodos de la clase Vector: a) add b) insertElementAt c) set d) remove e) removeAllElements f ) removeElementAt g) firstElement h) lastElement i) isEmpty j) contains k) indexOf l) size m) capacity 19.5 Explique por qué la operación de insertar elementos adicionales en un objeto Vector, cuyo tamaño actual sea menor que su capacidad, es una operación relativamente rápida, y por qué el insertar elementos adicionales en un objeto Vector, cuyo tamaño actual sea igual a la capacidad, es una operación relativamente baja. 19.6 Al extender la clase Vector, los diseñadores de Java pudieron crear la clase Stack rápidamente. ¿Cuáles son los aspectos negativos de este uso de la herencia, en especial para la clase Stack? Responda brevemente a las siguientes preguntas: a) ¿Cuál es la principal diferencia entre un objeto Set y un objeto Map? b) ¿Puede pasarse un arreglo bidimensional al método asList de Arrays? Si es así, ¿cómo se accedería a un elemento individual? c) ¿Qué ocurre cuando agregamos un valor de tipo primitivo (por ejemplo, double) a una colección? d) ¿Podemos imprimir todos los elementos en una colección sin utilizar un objeto Iterator? Si es así, explique cómo. 19.8 Explique brevemente la operación de cada uno de los siguientes métodos relacionados con Iterator: a) iterator b) hasNext c) next 19.9 Explique brevemente la operación de cada uno de los siguientes métodos de la clase HashMap: a) put b) get c) isEmpty d) containsKey e) keySet 19.10 Determine si cada uno de los siguientes enunciados es verdadero o falso. Si es falso, explique por qué. a) Los elementos en un objeto Collection deben almacenarse en orden ascendente, antes de poder realizar una búsqueda binaria mediante binarySearch. b) El método first obtiene el primer elemento en un objeto TreeSet. c) Un objeto List creado con el método asList de Arrays puede cambiar su tamaño. d) La clase Arrays proporciona el método static llamado sort para ordenar los elementos de un arreglo.

19.7

19.11 Explique la operación de cada uno de los siguientes métodos de la clase Properties: a) load b) store

www.elsolucionario.net

840

Capítulo 19 c) d)

Colecciones

getProperty list

19.12 Vuelva a escribir las líneas 17 a 26 en la figura 19.4 para que sean más concisas; utilice el método asList y el constructor de LinkedList que recibe un argumento Collection. 19.13 Escriba un programa que lea una serie de nombres de pila y los almacene en un objeto LinkedList. No almacene nombres duplicados. Permita al usuario buscar un nombre de pila. 19.14 Modifique el programa de la figura 19.20 para contar el número de ocurrencias de cada letra, en vez de cada palabra. Por ejemplo, la cadena "HOLA A TODOS" contiene una H, tres Os, una L, dos As, una T, una D y una S. Muestre los resultados. 19.15 Use un objeto HashMap para crear una clase reutilizable y elegir uno de los 13 colores predefinidos en la clase Color. Los nombres de los colores deben usarse como claves, y los objetos Color predefinidos deben usarse como valores. Coloque esta clase en un paquete que pueda importarse en cualquier programa en Java. Use su nueva clase en una aplicación que permita al usuario seleccionar un color y dibujar una figura en ese color. 19.16 Escriba un programa que determine e imprima el número de palabras duplicadas en un enunciado. Trate a las letras mayúsculas y minúsculas de igual forma. Ignore los signos de puntuación. 19.17 Vuelva a escribir su solución al ejercicio 17.8 para utilizar una colección LinkedList. 19.18 Vuelva a escribir su solución al ejercicio 17.9 para utilizar una colección LinkedList. 19.19 Escriba un programa que reciba una entrada tipo número entero de un usuario, y que determine si es primo. Si el número no es primo, muestre sus factores primos únicos. Recuerde que los factores de un número primo son sólo 1 y el mismo número primo. Todo número que no sea primo tiene una factorización prima única. Por ejemplo, considere el número 54. Los factores primos de 54 son 2, 3, 3 y 3. Cuando los valores se multiplican entre sí, el resultado es 54. Para el número 54, los factores primos a imprimir deben ser 2 y 3. Use objetos Set como parte de su solución. 19.20 Escriba un programa que utilice un objeto StringTokenizer para dividir en tokens una línea de texto introducida por el usuario, y que coloque cada token en un objeto TreeSet. Imprima los elementos del objeto TreeSet. [Nota: esto debe hacer que se impriman los elementos en orden ascendente]. 19.21 Los resultados de la figura 19.17 (PriorityQueueTest) muestra que PriorityQueue ordena elementos Douen orden ascendente. Vuelva a escribir la figura 19.17, de manera que ordene los elementos Double en orden descendente (es decir, 9.8 debe ser el elemento de mayor prioridad, en vez de 3.2). ble

www.elsolucionario.net

20 x Introducción a los applets de Java

Observe las medidas adecuadas, ya que de todas las cosas, la sincronización correcta es el factor más importante. — Hesiod

La pintura es el puente que vincula la mente del pintor con la del observador.

OBJETIVOS

— Eugene Delacroix

Q

Diferenciar entre applets (subprogramas) y aplicaciones.

Q

Observar algunas de las excitantes características de Java a través de los applets de demostración incluidos en el JDK.

Q

Escribir applets simples en Java.

Q

Escribir un documento HTML (HyperText Markup Language, Lenguaje de Marcado de Hipertexto) para cargar un applet en un contenedor de applets y ejecutarlo.

Q

Utilizar cinco métodos que el contenedor de un applet llama de manera automática durante el ciclo de vida del applet.

La dirección en la que la educación empiece a guiar a un hombre determinará su futuro en la vida.

En este capítulo aprenderá a:

— Platón

www.elsolucionario.net

Pla n g e ne r a l

842

Capítulo 20 Introducción a los applets de Java

20.1 Introducción 20.2 Applets de muestra incluidos en el JDK 20.3 Applet simple en Java: cómo dibujar una cadena 20.3.1 Cómo ejecutar un applet en el appletviewer 20.3.2 Ejecución de un applet en un explorador Web 20.4 Métodos del ciclo de vida de los applets 20.5 Cómo inicializar una variable de instancia con el método int 20.6 Modelo de seguridad “caja de arena” 20.7 Recursos en Internet y Web 20.8 Conclusión Resumen | Terminología | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

20.1 Introducción [Nota: este capítulo y sus ejercicios son pequeños y simples de manera intencional, para los lectores que desean aprender acerca de los applets después de leer sólo los primeros capítulos del libro; posiblemente los capítulos 2 y 3. En el capítulo 21, Multimedia: Applets y aplicaciones, en el capítulo 23, Subprocesamiento múltiple, y en el capítulo 24, Redes, presentaremos applets más complejos]. En este capítulo se introducen los applets: programas en Java que pueden incrustarse en documentos HTML (Lenguaje de marcado de hipertexto) (es decir, páginas Web). Cuando un explorador carga una página Web que contiene un applet, éste se descarga en el explorador Web y se ejecuta. Al explorador que ejecuta un applet se le conoce como contenedor de applets. El JDK incluye el contenedor de applets appletviewer para probar applets a medida que se van desarrollando, y antes de incrustarlas en las páginas Web. Por lo general, demostraremos los applets mediante el uso del appletviewer. Si desea ejecutar sus applets en un explorador Web, debe estar consciente de que algunos exploradores Web no soportan a Java de manera predeterminada. Puede visitar java.com y hacer clic en el botón Descargar AHORA para instalar Java en su explorador Web. Hay soporte para varios exploradores Web populares.

20.2 Applets de muestra incluidos en el JDK Comencemos por considerar varios applets de muestra que se incluyen con el JDK. Cada applet de muestra incluye su código fuente. Algunos programadores encuentran interesante leer este código fuente para aprender nuevas y excitantes características sobre Java. demuestran una pequeña porción de las poderosas herramientas de Java. Los programas de demostración que se proporcionan con el JDK se encuentran en un directorio llamado demo. Para Windows, la ubicación predeterminada del directorio demo del JDK 6.0 es C:\Archivos de programa\Java\jdk1.6.0\demo

En UNIX/Linux/Mac OS X, la ubicación predeterminada es el directorio en el que usted haya instalado el JDK, seguido de jdk1.6.0/demo. Por ejemplo, /usr/local/jdk1.6.0/demo

En las demás plataformas hay una estructura similar de directorios (o carpetas). Este capítulo supone que el JDK está instalado en C:\Archivos de programa\Java\jdk1.6.0_01\demo en Windows, y en su directorio personal ~/jdk1.6.0 en UNIX/Linux/Mac OS X. Tal vez necesite actualizar las ubicaciones que se especifican aquí para reflejar el directorio de instalación y la unidad de disco que usted eligió, o una versión distinta del JDK. Si utiliza una herramienta de desarrollo de Java que no incluya los programas de muestra de Sun Java, puede descargar el JDK (con los demos) en el sitio Web de Java de Sun Microsystems java.sun.com/javase/6/

www.elsolucionario.net

20.2

Applets de muestra incluidos en el JDK

843

Applet TresEnRaya El applet de demostración TresEnRaya (también conocido como gato) le permite a usted jugar contra la computadora. Para ejecutar este applet, abra una ventana de comandos y vaya al directorio demo del JDK. El directorio demo contiene varios subdirectorios. Puede ver estos directorios escribiendo el comando dir en la ventana de comandos en Windows, o el comando ls en UNIX/Linux/Mac OS X. Hablaremos sobre los programas de muestra en los directorios applets y jfc. El directorio applets contiene varios applets de demostración. El directorio jfc (Java Foundation Classes, Clases Fundamentales de Java) contiene applets y aplicaciones que demuestran las características de gráficos y GUI de Java. Cambie al directorio applets y muestre su contenido para ver los nombres de los directorios para los applets de demostración. En la figura 20.1 se proporciona una breve descripción de cada applet de muestra. Si su explorador Web soporta Java, puede probar estos applets abriendo el sitio Web java.sun.com/javase/6/docs/technotesamples/demos.html en su explorador y haciendo clic en el vínculo Applets Page. Demostraremos tres de estos applets usando el comando appletviewer en una ventana de comandos.

Ejemplo

Descripción

Animator

Realiza una de cuatro animaciones separadas.

ArcTest

Demuestra cómo dibujar arcos. Puede interactuar con el applet para modificar los atributos del arco que se muestra en pantalla.

BarChart

Dibuja un gráfico de barras simple.

Blink

Muestra texto destellante en distintos colores.

CardTest

Demuestra varios componentes y esquemas de la GUI.

Clock

Dibuja un reloj con manecillas giratorias, la fecha actual y la ahora actual. El reloj se actualiza una vez por segundo.

DitherTest

Demuestra cómo dibujar con una técnica de gráficos conocida como difuminado, la cual permite una transformación gradual de un color a otro.

DrawTest

Permite usar el ratón para dibujar líneas y puntos en distintos colores, arrastrando el ratón.

Fractal

Dibuja un fractal. Por lo general, los fractales requieren cálculos complejos para determinar la forma en que se muestran en la pantalla.

GraphicsTest

Dibuja figuras para ilustrar las herramientas de gráficos.

GraphLayout

Dibuja un gráfico que consiste en muchos nodos (representados como rectángulos) conectados por líneas. Arrastre un nodo para ver cómo se ajustan los demás nodos en el gráfico en la pantalla, y demostrar las interacciones gráficas complejas.

ImageMap

Demuestra una imagen con puntos activos. Al posicionar el puntero del ratón sobre ciertas áreas de la imagen se resalta el área y se muestra un mensaje en la esquina inferior izquierda de la ventana del contenedor de applets. Colóquese sobre la boca para escuchar cómo la imagen dice “hola.”

JumpingBox

Desplaza un rectángulo en forma aleatoria, alrededor de la pantalla. ¡Trate de atraparlo haciendo clic sobre él con el ratón!

MoleculeViewer

Presenta una vista tridimensional de varias moléculas químicas distintas. Arrastre el ratón y verá la molécula desde varios ángulos.

NervousText

Arrastra texto que salta por la pantalla.

SimpleGraph

Dibuja una curva compleja.

Figura 20.1 | Los ejemplos del directorio applets. (Parte 1 de 2).

www.elsolucionario.net

844

Capítulo 20 Introducción a los applets de Java

Ejemplo

Descripción

SortDemo

Compara tres técnicas de ordenamiento. El ordenamiento (que se describe en el capítulo 16) sirve para organizar la información; es como alfabetizar las palabras. Cuando usted ejecuta este ejemplo desde una ventana de comandos, aparecen tres ventanas del appletviewer. Cuando ejecuta este ejemplo en un explorador Web, los tres ejemplos aparecen uno al lado del otro. Haga clic en cada una de ellas para empezar con el ordenamiento. Observe que cada una de las tres técnicas de ordenamiento operan a distintas velocidades.

SpreadSheet

Muestra una hoja de cálculo simple, con filas y columnas.

TicTacToe

Permite al usuario jugar al Tres en raya contra la computadora.

WireFrame

Dibuja una figura tridimensional como una malla de alambre. Arrastre el ratón para ver la figura desde distintos ángulos.

Figura 20.1 | Los ejemplos del directorio applets. (Parte 2 de 2).

Cambie al subdirectorio TicTacToe, en donde encontrará el documento HTML utiliza para ejecutar el applet. En la ventana de comandos, escriba el comando

example1.html

que se

appletviewer example1.html

y oprima la tecla Entrar. Esto hace que se ejecute el contenedor de applets appletviewer, el cual carga el documento HTML example1.html que se especifica como su argumento de línea de comandos. El appletviewer determina en base al documento qué applet debe cargar y comienza a ejecutarlo. La figura 20.2 muestra varias capturas de pantalla del juego de Tres en raya con este applet.

Figura 20.2 | Ejecución de ejemplo del applet Tres en raya. Usted es el jugador X. Para interactuar con el applet, coloque el ratón sobre el cuadro en el que desea colocar una X y haga clic con el botón del ratón. El applet reproduce un sonido y coloca una X en el cuadro, si éste está libre. Si el cuadro está ocupado, es un movimiento inválido y el applet reproduce un sonido distinto, indicando que usted no puede hacer el movimiento especificado. Después de que haga un movimiento válido, el applet responderá con su propio movimiento. Para jugar de nuevo, haga clic en el menú Subprograma (Applet) del appletviewer y seleccione el elemento de menú Volver a cargar (Reload) (Figura 20.3). Para terminar el appletviewer, haga clic en el menú Subprograma y seleccione el elemento de menú Salir (Quit).

Applet DrawTest El applet de demostración DrawTest le permite dibujar líneas y puntos en distintos colores. En la ventana de comandos, cambie al directorio applets y después al subdirectorio DrawTest. Puede desplazarse hacia arriba del árbol de directorios para llegar a demo, mediante el comando “cd ..” en la ventana de comandos. El directorio DrawTest contiene el documento example1.html que se utiliza para ejecutar el applet. En la ventana de comandos, escriba el comando appletviewer example1.html

www.elsolucionario.net

20.2

Applets de muestra incluidos en el JDK

845

y oprima la tecla Entrar. El appletviewer carga example1.html, determina en base a este archivo qué applet cargar y comienza a ejecutarlo. La figura 20.4 muestra una captura de pantalla de este applet, después de dibujar algunas líneas y puntos. De manera predeterminada, el applet nos permite dibujar líneas de color negro, arrastrando el ratón a lo largo del applet. Al arrastrar el ratón, observe que el punto inicial de la línea siempre permanece en el mismo lugar, y el punto final de la línea sigue al ratón a lo largo del applet. La línea no es permanente sino hasta que se libera el botón del ratón. Para seleccionar un color, haga clic en uno de los botones de opción en la parte inferior del applet. Puede seleccionar de entre rojo, verde, azul, rosa, naranja y negro. Cambie la figura a dibujar de líneas (Lines) a (Points) al seleccionar Points en el cuadro combinado. Para empezar un nuevo dibujo, seleccione Volver a cargar en el menú Subprograma del appletviewer.

Seleccione Volver el applet para ejecutarlo de nuevo a cargar

Seleccione Salir para terminar el appletviewer

Figura 20.3

| Menú applet en el appletviewer.

Arrastre el ratón en el área en blanco para dibujar

Seleccione Lineas o Puntos del cuadro combinado para especificar qué se dibujará cuando usted arrastre el ratón

Seleccione el color de dibujo haciendo clic en uno de los botones de opción

Figura 20.4 | Ejecución de ejemplo del applet DrawTest.

www.elsolucionario.net

846

Capítulo 20 Introducción a los applets de Java

Applet Java2D Este applet demuestra muchas características de la API Java2D (que presentamos en el capítulo 12). Cambie al directorio jfc que se encuentra en el directorio demo del JDK, y después cambie al directorio Java2D. En la ventana de comandos, escriba el comando appletviewer Java2Demo.html

y oprima Intro. El appletviewer carga Java2Demo.html, determina en base al documento qué applet debe cargar y comienza a ejecutarlo. En la figura 20.5 se muestra una captura de pantalla de una de las muchas demostraciones de este applet, en relación con las herramientas para gráficos bidimensionales de Java. En la parte superior del applet hay fichas que parecen carpetas en un archivero. Esta demostración cuenta con 12 fichas, en cada una de las cuales se demuestran las características de la API Java 2D. Para cambiar a una parte distinta de la demostración, simplemente haga clic en otra ficha. También puede probar cambiando las opciones en la esquina superior derecha del applet. Algunas de estas opciones afectan la velocidad con la que el applet dibuja los gráficos. Por ejemplo, haga clic en la casilla de verificación que está a la izquierda de la palabra Anti-Aliasing para activar y desactivar el suavizado (una técnica de gráficos para producir gráficos en pantalla más suaves, en los que los bordes del gráfico están desenfocados). Cuando esta característica se desactiva, la velocidad de animación aumenta para las figuras animadas que están en la parte inferior de la demostración (figura 20.5). Este incremento en el rendimiento ocurre debido a que las figuras que no están suavizadas son menos complejas de dibujar. Haga clic en una ficha para seleccionar una demostración de gráficos bidimensionales

Pruebe cambiar las opciones para ver su efecto en la demostración

Figura 20.5 | Ejecución de ejemplo del applet Java2D.

20.3 Applet simple en Java: cómo dibujar una cadena Todo applet en Java es una interfaz gráfica de usuario, en la que podemos colocar componentes de GUI mediante el uso de las técnicas presentadas en el capítulo 11, o dibujar mediante el uso de las técnicas demostradas en el

www.elsolucionario.net

20.3

Applet simple en Java: cómo dibujar una cadena

847

capítulo 12. En este capítulo demostraremos cómo dibujar en un applet. Los ejemplos en los capítulos 21, 23 y 24 demuestran cómo crear la interfaz gráfica de usuario de un applet. Ahora crearemos nuestro propio applet. Comenzaremos con un applet simple (figura 20.6) que dibuja "Bienvenido a la programación en Java!". En la figura 20.7 se muestra a este applet ejecutándose en dos contenedores de applets: el appletviewer y el explorador Web Microsoft Internet Explorer. Al final de esta sección explicaremos cómo ejecutar el applet en un explorador Web.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

// Fig. 20.6: BienvenidoApplet.java // Su primer applet en Java. import java.awt.Graphics; // el programa utiliza la clase Graphics import javax.swing.JApplet; // el programa utiliza la clase JApplet public class BienvenidoApplet extends JApplet { // dibuja el texto en el fondo del applet public void paint( Graphics g ) { // llama a la versión del método paint de la superclase super.paint( g ); // dibuja un objeto String en la coordenada x 25 y la coordenada y 25 g.drawString( "Bienvenido a la programacion en Java!", 25, 25 ); } // fin del método paint } // fin de la clase BienvenidoApplet

Figura 20.6 | Applet que dibuja una cadena.

BienvenidoApplet

ejecutándose en el appletviewer

Eje x Eje y La esquina superior izquierda del área de dibujo es la ubicación (0,0). El área de dibujo se extiende desde debajo del menú Subprogramas, hasta antes de la barra de estado. Las coordenadas x se incrementan de izquierda a derecha; las coordenadas y se incrementan de arriba hacia abajo

Menú Subprograma La barra de estado imita lo que se mostraría en la barra de estado del explorador Web, a medida que el applet se carga y comienza a ejecutarse Coordenada del píxel (25, 25) en la que se muestra la cadena

BienvenidoApplet

ejecutándose en Microsoft Internet Explorer

Esquina superior izquierda del área de dibujo Coordenada del píxel (25, 25)

Barra de estado

Figura 20.7 | Resultados de ejemplo del applet BienvenidoApplet en la figura 20.6.

www.elsolucionario.net

848

Capítulo 20 Introducción a los applets de Java

Creación de la clase Applet En la línea 3 se importa la clase Graphics para permitir al applet dibujar gráficos, como líneas, rectángulos, óvalos y cadenas de caracteres. La clase JApplet (que se importa en la línea 4) del paquete javax.swing se utiliza para crear applets. Al igual que con las aplicaciones, todo applet de Java contiene por lo menos una declaración de clase public. Un contenedor de applets sólo puede crear objetos de clases que sean public y extiendan a JApplet ¿o a la clase Applet de las versiones anteriores de Java? Por esta razón, la clase BienvenidoApplet (líneas 6 a 17) extiende a JApplet. Un contenedor de applets espera que todo applet de Java tenga métodos llamados init, start, paint, stop y destroy, cada uno de los cuales está declarado en la clase JApplet. Cada nueva clase de applet que crea el programador hereda las implementaciones predeterminadas de estos métodos de la clase JApplet. Estos métodos se pueden sobrescribir (redefinir) para realizar tareas específicas de cada applet. En la sección 20.4 veremos cada uno de estos métodos con más detalle. Cuando un contenedor de applets carga la clase BienvenidoApplet, el contenedor crea un objeto de tipo BienvenidoApplet, y después llama a tres de los métodos del applet. En secuencia, estos tres métodos son: init, start y paint. Si no declaramos estos métodos en el applet, el contenedor de applets llama a las versiones heredadas. Los métodos init y start de la superclase tienen cuerpos vacíos, por lo que no realizan ninguna tarea. El método paint de la superclase no dibuja nada en el applet. Tal vez usted se pregunte por qué es necesario heredar los métodos init, start y paint si sus implementaciones predeterminadas no realizan tareas. Algunos applets no utilizan todos estos métodos. Sin embargo, el contenedor de applets no sabe eso. Por ende, espera que todo applet tenga estos métodos, de manera que pueda proporcionar una secuencia de inicio consistente para cada applet. Esto es similar al hecho de que las aplicaciones siempre empiezan su ejecución con main. Al heredar las versiones “predeterminadas” de estos métodos, se garantiza que el contenedor de applets podrá ejecutar cada applet de manera uniforme. Además, al heredar las implementaciones predeterminadas de estos métodos, el programador puede concentrarse en definir sólo los métodos requeridos para un applet específico.

Cómo sobrescribir el método paint para dibujar Para permitir que nuestro applet dibuje, la clase BienvenidoApplet sobrescribe el método paint (líneas 9 a 16), al colocar instrucciones en el cuerpo de paint que dibujan un mensaje en la pantalla. El método paint recibe un parámetro de tipo Graphics (al cual se le llama g por convención), el cual se utiliza para dibujar gráficos en el applet. No se llama explícitamente al método paint en un applet. En vez de ello, el contenedor de applets llama a paint para indicar al applet cuándo dibujar, y el contenedor de applets es responsable de pasar un objeto Graphics como argumento. En la línea 12 se hace una llamada a la versión del método paint de la superclase, que se heredó de JApplet. Esta instrucción debe ser la primera instrucción en el método paint de todo applet. Si se omite podría provocar errores sutiles de dibujo en los applets que combinen el dibujo con componentes de la GUI. En la línea 15 se utiliza el método drawString de Graphics para dibujar Bienvenido a la programacion en Java! en el applet. Este método recibe como argumentos el objeto String a dibujar y las coordenadas x-y en las que debe aparecer la esquina inferior izquierda del objeto String en el área de dibujo. Cuando se ejecuta la línea 15, dibuja el objeto String en el applet, en las coordenadas 25 y 25.

20.3.1 Cómo ejecutar un applet en el appletviewer Al igual que con las clases de aplicaciones, debemos compilar una clase de applet antes de poder ejecutarla. Después de crear la clase BienvenidoApplet y guardarla en el archivo BienvenidoApplet.java, abra una ventana de comandos, cambie al directorio en el que guardó la declaración de la clase de applet y compile la clase BienvenidoApplet. Recuerde que los applets están incrustados en páginas Web para ejecutarlos en un contenedor de applets (appletviewer o un explorador Web). Antes de poder ejecutar el applet, debe crear un documento HTML (Lenguaje de marcado de hipertexto) que especifique cuál applet ejecutar en el contenedor de applets. Por lo general, un documento HTML termina con la extensión de archivo “.html” o “.htm”. En la figura 20.8 se muestra un documento HTML simple (BienvenidoApplet.html) que carga el applet definido en la figura 20.6, en un contenedor de applets. [Nota: si está interesado en aprender más acerca de HTML, el CD que se incluye en este

www.elsolucionario.net

20.3

Applets simple en Java: cómo dibujar una cadena

849

libro contiene tres capítulos de nuestro libro Internet and World Wide Web How to Program, Tercera edición, que introducen la versión actual de HTML (conocida como XHTML) y la herramienta de formato de páginas Web conocida como Hojas de estilo en cascada (CSS)]. La mayoría de los elementos de HTML se delimitan mediante pares de etiquetas. Por ejemplo, las líneas 1 y 4 de la figura 20.8 indican el inicio y el fin, respectivamente, del documento HTML. Todas las etiquetas de HTML empiezan con un signo . En las líneas 2 y 3 se especifica un elemento applet, el cual indica al contenedor de applets que debe cargar un applet específico y define el tamaño del área de visualización del applet (su anchura y altura en píxeles) en el contenedor de applets. Por lo general, el applet y su correspondiente documento HTML se almacenan en el mismo directorio en el disco. Comúnmente, un explorador Web carga un documento HTML de una computadora (distinta a la del lector) conectada a Internet. Sin embargo, los documentos HTML también pueden residir en su computadora (como vio en la sección 20.2). Cuando un contenedor de applets encuentra un documento HTML que contiene un applet, el contenedor carga de manera automática el archivo (o archivos) .class del applet, del mismo directorio en la computadora en donde reside el documento HTML. El elemento applet tiene varios atributos. El primer atributo en la línea 2, code = "BienvenidoApplet. class", indica que el archivo BienvenidoApplet.class contiene la clase de applet compilada. Los atributos segundo y tercero en la línea 2 indican la anchura (width) de 300 y la altura (height) de 45 del applet, en píxeles. La etiqueta (línea 3) termina el elemento applet que empezó en la línea 2. La etiqueta (línea 4) termina el documento HTML. Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 20.1 Para asegurar que un applet pueda verse apropiadamente en la mayoría de las pantallas de computadora, generalmente debe ser menor de 1024 píxeles de ancho y de 768 píxeles de alto (las medidas soportadas por la mayoría de las pantallas de computadora).

Error común de programación 20.1 Olvidar la etiqueta de cierre evita que el applet se ejecute en ciertos contenedores de applets. El applet-viewer termina sin indicar un error. Algunos exploradores Web simplemente ignoran el elemento applet incompleto.

1 2 3 4



Figura 20.8 |

BienvenidoApplet.html

carga a BienvenidoApplet (figura 20.6) en un contenedor de applets.

Tip para prevenir errores 20.1 Si recibe un mensaje de error MissingResourceException al cargar un applet en el appletviewer o en un explorador Web, compruebe la etiqueta en el documento HTML cuidadosamente para detectar errores de sintaxis, como las comas (,) entre los atributos.

El appletviewer sólo comprende las etiquetas de HTML y e ignora a todas las demás etiquetas en el documento. El appletviewer es un sitio ideal para probar un applet y asegurar que se ejecute en forma apropiada. Una vez que se verifique la ejecución del applet, puede agregar sus etiquetas de HTML a una página Web que otros puedan ver en sus exploradores Web. Para ejecutar BienvenidoApplet en el appletviewer, abra una ventana de comandos, cambie al directorio que contiene su applet y su documento HTML, y después escriba appletviewer BienvenidoApplet.html

www.elsolucionario.net

850

Capítulo 20 Introducción a los applets de Java

Tip para prevenir errores 20.2 Pruebe sus applets en el contenedor de applets appletviewer antes de ejecutarlos en un explorador Web. A menudo, los exploradores Web guardan una copia de un applet en memoria, hasta que se cierran todas sus ventanas. Si modifica un applet, lo vuelve a compilar y después lo carga en su explorador Web, éste podría seguir ejecutando la versión original del applet. Cierre todas las ventanas del explorador Web para eliminar el applet anterior de la memoria. Abra una nueva ventana del explorador Web y cargue su applet para ver los cambios.

Tip para prevenir errores 20.3 Pruebe sus applets en todos los exploradores Web en los que se ejecutará, para asegurar que operen en forma correcta.

20.3.2 Ejecución de un applet en un explorador Web Las ejecuciones de los programas de ejemplo de la figura 20.6 demuestran cómo se ejecuta BienvenidoApplet en el appletviewer y en el explorador Web Microsoft Internet Explorer. Para ejecutar un applet en Internet Explorer, realice los siguientes pasos: 1. Seleccione Abrir… en el menú Archivo. 2. En el cuadro de diálogo que se despliegue, haga clic en el botón Examinar…. 3. Localice el directorio que contenga el documento de HTML para el applet que desee ejecutar. 4. Seleccione el documento de HTML. 5. Haga clic en el botón Abrir. 6. Haga clic en el botón Aceptar. [Nota: los pasos para ejecutar applets en otros exploradores Web son similares]. Si su applet se ejecuta en el appletviewer, pero no se ejecuta en su explorador Web, tal vez Java no esté instalado y configurado para su explorador Web. En este caso, visite el sitio Web java.com y haga clic en el botón Descargar AHORA para instalar Java para su explorador Web. En Internet Explorer, si esto no corrige el problema, tal vez necesite configurar manualmente Internet Explorer para que utilice Java. Para ello, haga clic en el menú Herramientas y seleccione Opciones de Internet…, y después haga clic en la ficha Opciones avanzadas en la ventana que aparezca. Localice la opción “Utilizar JRE v1.6.0 para (es necesario reiniciar)” y asegúrese que esté seleccionada, después haga clic en Aceptar. Cierre todas las ventanas de su explorador Web antes de volver a intentar ejecutar otro applet.

20.4 Métodos del ciclo de vida de los applets Ahora que ha creado un applet, vamos a considerar los cinco métodos de applet a los que llama el contenedor de applets, desde el momento en el que se carga el applet en el explorador Web, hasta el momento en el que éste termina el applet. Estos métodos corresponden a diversos aspectos del ciclo de vida de un applet. En la figura 20.9 se enlistan estos métodos, que las clases de applets heredan de la clase JApplet. La tabla especifica el momento en el que se llama a cada método y explica su propósito. A excepción del método paint, estos métodos tienen cuerpos vacíos de manera predeterminada. Si desea declarar alguno de estos métodos en sus applets y hacer que el contenedor de applets los llame, debe usar los encabezados de los métodos que se muestran en la figura 20.9. Si modifica los encabezados de los métodos (por ejemplo, cambiar los nombres de los métodos o proporcionar

Método

Momento en que se llama al método y su propósito

public void init()

El contenedor de applets lo llama una vez, cuando se carga un applet para ejecutarlo. Este método inicializa un applet. Las acciones comunes que se realizan aquí son: inicializar campos, crear componentes de GUI, cargar los sonidos a reproducir, cargas las imágenes a visualizar (vea el capítulo 20, Multimedia: applets y aplicaciones) y crear subprocesos (vea el capítulo 23, Subprocesamiento múltiple).

Figura 20.9 | Métodos del ciclo de vida de JApplet, que un contenedor de applets llama durante la ejecución de un applet. (Parte 1 de 2).

www.elsolucionario.net

20.5

Método

Cómo inicializar una variable de instancia con el método int

851

Momento en que se llama al método y su propósito

public void start()

El contenedor de applets lo llama una vez que el método init termina su ejecución. Además, si el usuario explora otro sitio Web y después regresa a la página HTML del applet, el método start se llama de nuevo. Este método realiza todas las tareas que deben completarse cuando el applet se carga por primera vez, y que deben realizarse cada vez que se vuelva a visitar la página HTML del applet. Las acciones que se realizan aquí podrían incluir: iniciar una animación (vea el capítulo 21) o iniciar otros subprocesos de ejecución (vea el capítulo 23). public void paint( Graphics g )

El contenedor de applets lo llama después de los métodos init y start. El método paint también se llama cuando el applet necesita volver a visualizarse. Por ejemplo, si el usuario cubre el applet con otra ventana abierta en la pantalla, y más adelante descubre el applet, se hace una llamada al método paint. Las acciones comunes que se realizan aquí incluyen: dibujar con el objeto Graphics g que el contenedor de applets pasa al método paint. public void stop()

El contenedor de applets lo llama cuando el usuario sale de la página Web del applet para ir a explorar otra página Web. Como es posible que el usuario regrese a la página Web que contiene el applet, el método stop realiza tareas que podrían requerirse para suspender la ejecución del applet, de manera que no utilice tiempo de procesamiento de la computadora cuando no esté visualizado en la pantalla. Las acciones comunes que se realizan en este método detendrían la ejecución de animaciones y subprocesos. public void destroy()

El contenedor de applets lo llama cuando el applet se va a eliminar de la memoria. Esto ocurre cuando el usuario sale de la sesión de navegación, cerrando todas las ventanas del explorador Web, y también puede ocurrir a discreción del explorador Web, cuando el usuario ha navegado hacia otras páginas Web. El método realiza cualquier tarea que se requiera para limpiar los recursos asignados al applet.

Figura 20.9 | Métodos del ciclo de vida de JApplet, que un contenedor de applets llama durante la ejecución de un applet. (Parte 2 de 2). parámetros adicionales), el contenedor de applets no llamará a sus métodos. En vez de ello, llamará a los métodos de la superclase, heredados de JApplet.

Error común de programación 20.2 Si declara los métodos init, start, paint, stop o destroy con encabezados que sean distintos a los que se muestran en la figura 20.9, el contenedor de applets no los llamará. El código especificado en sus versiones de los métodos no se ejecutará.

20.5 Cómo inicializar una variable de instancia con el método init

Nuestro siguiente applet (figura 20.10) calcula la suma de dos valores introducidos por el usuario, y muestra el resultado arrastrando un objeto String dentro de un rectángulo en el applet. La suma se almacena en una varia-

1 2 3 4 5

// Fig. 20.10: SumaApplet.java // Suma de dos números de punto import java.awt.Graphics; import javax.swing.JApplet; import javax.swing.JOptionPane;

flotante. // el programa usa la clase Graphics // el programa usa la clase JApplet // el programa usa la clase JOptionPane

Figura 20.10 | Suma de valores double. (Parte 1 de 2).

www.elsolucionario.net

852

6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

Capítulo 20 Introducción a los applets de Java

public class SumaApplet extends JApplet { private double suma; // suma de los valores introducidos por el usuario // inicializa el applet, obteniendo los valores del usuario public void init() { String primerNumero; // primera cadena introducida por el usuario String segundoNumero; // segunda cadena introducida por el usuario double numero1; // primer número a sumar double numero2; // segundo número a sumar // obtiene el primer número del usuario primerNumero = JOptionPane.showInputDialog( "Escriba el primer valor de punto flotante" ); // obtiene el segundo número del usuario segundoNumero = JOptionPane.showInputDialog( "Escriba el segundo valor de punto flotante" ); // convierte los números del tipo String al tipo double numero1 = Double.parseDouble( primerNumero ); numero2 = Double.parseDouble( segundoNumero ); suma = numero1 + numero2; // suma los números } // fin del método init // dibuja los resultados en un rectángulo, en el fondo del applet public void paint( Graphics g ) { super.paint( g ); // llama a la versión del método paint de la superclase // dibuja un rectángulo empezando desde (15, 10), que tenga 270 // píxeles de ancho y 20 píxeles de alto g.drawRect( 15, 10, 270, 20 ); // dibuja los resultados como un objeto String en (25, 25) g.drawString( "La suma es " + suma, 25, 25 ); } // fin del método paint } // fin de la clase SumaApplet

Figura 20.10 | Suma de valores double. (Parte 2 de 2).

www.elsolucionario.net

20.7

Recursos en Internet y Web

853

ble de instancia de la clase SumaApplet, para que pueda utilizarse tanto en el método init como en el método paint. El documento HTML que carga este applet en el appletviewer se muestra en la figura 20.11.

1 2 3 4



Figura 20.11 |

SumaApplet.html

carga la clase SumaApplet de la figura 20.10 dentro de un contenedor de applets.

El applet solicita al usuario dos números de punto flotante. En la línea 9 (figura 20.10) se declara la variable de instancia suma de tipo double. El applet contiene dos métodos: init (líneas 12 a 33) y paint (líneas 36 a 46). Cuando un contenedor de applets carga este applet, el contenedor crea una instancia de la clase SumaApplet y llama a su método init; esto ocurre sólo una vez durante la ejecución de un applet. Por lo común, el método init inicializa los campos del applet (si necesitan inicializarse con valores que no sean los predeterminados) y realiza otras tareas que sólo deben ocurrir una vez, cuando el applet empieza a ejecutarse. La primera línea de init siempre aparece como se muestra en la línea 12, la cual indica que init es un método public que no recibe argumentos y no devuelve información cuando termina su ejecución. En las líneas 14 a 30 se declaran variables para almacenar los valores introducidos por el usuario, obtener la entrada del usuario y convertir los objetos String introducidos por el usuario en valores double. La instrucción de asignación en la línea 32 suma los valores almacenados en las variables numero1 y numero2, y asigna el resultado a la variable de instancia suma. En este punto, el método init del applet devuelve el control del programa al contenedor de applets, el cual a su vez llama al método start del applet. No declaramos a start en este applet, por lo que aquí se hace la llamada al método heredado de la clase JApplet. En los capítulos 21 y 23 veremos usos comunes del método start. A continuación, el contenedor de applets llama al método paint del applet, el cual dibuja un rectángulo (línea 42) en donde aparecerá el resultado de la suma. En la línea 45 se hace una llamada al método drawString del objeto Graphics para visualizar los resultados. La instrucción concatena el valor de la variable de instancia suma al objeto String "La suma es " y muestra en pantalla el objeto String concatenado.

Observación de ingeniería de software 20.1 Las únicas instrucciones que se deben colocar en el método init de un applet son las que deben ejecutarse sólo una vez, al inicializar el applet.

20.6 Modelo de seguridad “caja de arena” Sería peligroso permitir a los applets que, por lo general, se descargan de Internet, leer y escribir archivos en una computadora cliente, o acceder a otros recursos del sistema. Por ejemplo, ¿qué ocurriría si usted descargara un applet malicioso? La plataforma Java utiliza el modelo de seguridad “caja de arena” para evitar que el código que usted descargue en su equipo local acceda a los recursos del sistema local, como los archivos. El código que se ejecuta dentro de la “caja de arena” no tiene permitido “jugar fuera de la caja”. Para obtener más información acerca de la seguridad y los applets, visite developer.java.sun.com/developer/technicalArticles/Security/Signed

Para obtener información acerca del modelo de seguridad de la Plataforma Java 2, visite java.sun.com/javase/6/docs/technotes/guides/security/index.html

20.7 Recursos en Internet y Web Si tiene acceso a Internet, hay una gran cantidad de recursos sobre applets de Java disponibles para usted. El mejor lugar para empezar es el de origen: el sitio Web de Java de Sun Microsystems, java.sun.com. La página Web java.sun.com/applets

www.elsolucionario.net

854

Capítulo 20 Introducción a los applets de Java

contiene varios recursos sobre applets de Java, incluyendo los applets de muestra del JDK y otros applets (muchos de los cuales se pueden descargar). Si no tiene Java instalado y configurado para su explorador Web, puede visitar java.com

y hacer clic en el botón Descargar AHORA para descargar e instalar Java en su explorador Web. Se proporcionan instrucciones para varias versiones de Windows, Linux, Solaris y Mac OS. El sitio Web de Java de Sun Microsystems java.sun.com

incluye soporte técnico, foros de discusión, artículos técnicos, recursos, anuncios de nuevas características de Java y un acceso anticipado a las nuevas tecnologías de Java. Para ver varios tutoriales en línea gratuitos, visite el sitio Web java.sun.com/learning

Otro sitio Web útil es JARS (conocido originalmente como Servicio de clasificación de applets de Java). El sitio Web de JARS www.jars.com

era un almacén de applets de Java que clasificaba cada applet registrado en el sitio, de manera que los visitantes pudieran ver los mejores applets en Web. En las primeras etapas del desarrollo del lenguaje Java, lograr que su applet se clasificara aquí era una excelente forma de demostrar sus habilidades de programación en Java. JARS es ahora un sitio de clasificaciones sobre todo lo relacionado con Java para programadores. Los recursos que se enlistan en esta sección proporcionan hipervínculos hacia muchos otros sitios Web relacionados con Java. Invierta tiempo explorando estos sitios, ejecutando applets y leyendo el código fuente de éstos cuando esté disponible. Esto le ayudará a expandir con rapidez su conocimiento sobre Java.

20.8 Conclusión En este capítulo aprendió los fundamentos de los applets de Java. Aprendió los conceptos básicos sobre HTML, que le permiten incrustar un applet en una página Web y ejecutarlo en un contenedor de applets, como appletviewer o un explorador Web. Además, aprendió acerca de los cinco métodos que el contenedor de applets llama de manera automática durante el ciclo de vida de un applet. En el siguiente capítulo verá varios applets adicionales, a medida que vayamos presentando las herramientas básicas de multimedia. En el capítulo 23, Subprocesamiento múltiple, verá un applet con los métodos start y stop que se utilizan para controlar varios subprocesos de ejecución. En el capítulo 24, Redes, le demostraremos cómo personalizar un applet a través de parámetros que se especifiquen en un elemento HTML del applet.

Resumen Sección 20.1 Introducción • Los applets son programas en Java que pueden incrustarse en documentos HTML. • Cuando un explorador Web carga una página Web que contiene un applet, éste se descarga en el explorador Web y se ejecuta. • El explorador Web que ejecuta un applet se conoce como el contenedor de applets. El JDK incluye el contenedor de applets appletviewer, para probar applets antes de incrustarlos en una página Web.

Sección 20.2 Applets de muestra incluidos en el JDK • Para volver a ejecutar un applet en el appletviewer, haga clic en el menú Subprograma y seleccione el elemento de menú Volver a cargar. • Para terminar el appletviewer, seleccione el elemento de menú Salir del menú Subprograma.

www.elsolucionario.net

Resumen

855

Sección 20.3 Applet simple en Java: cómo dibujar una cadena • Todo applet de Java es una interfaz gráfica de usuario, en la cual podemos colocar componentes de GUI o dibujar. • La clase JApplet del paquete javax.swing se utiliza para crear applets. • Un contenedor de applets sólo puede crear objetos de clases que sean public y extiendan a JApplet (o la clase Applet de las versiones anteriores de Java). • Un contenedor de applets espera que cada applet de Java tenga métodos llamados init, start, paint, stop y destroy, cada uno de los cuales está declarado en la clase JApplet. Cada nueva clase de applet que usted cree hereda las implementaciones predeterminadas de estos métodos de la clase JApplet. • Cuando un contenedor de applets carga un applet, el contenedor crea un objeto del tipo del applet, y después llama a los métodos init, start y paint del applet. Si no declara estos métodos en su applet, el contenedor de applets llama a las versiones heredadas. • Los métodos init y start de la superclase tienen cuerpos vacíos, por lo cual no realizan ninguna tarea. El método paint de la superclase no dibuja nada en el applet. • Para permitir a un applet dibujar, hay que sobrescribir su método paint. No debemos llamar explícitamente al método paint en un applet. En vez de ello, el contenedor de applet debe llamar a paint para indicar al applet cuándo debe dibujar, y el contenedor de applets es responsable de pasarle un objeto Graphics como argumento. • La primera instrucción en el método paint debe ser una llamada al método paint de la superclase. Omitir esto puede provocar errores sutiles de dibujo en applets que combinan el dibujo y componentes de GUI. • Antes de ejecutar un applet, debemos crear un documento HTML (Lenguaje de marcado de hipertexto) que especifique cuál applet ejecutar en el contenedor de applets. Por lo general, un documento HTML termina con una extensión de archivo “.html” o “.htm”. • La mayoría de los elementos de HTML se delimitan mediante pares de etiquetas. Todas las etiquetas de HTML empiezan con un signo . • Un elemento applet indica al contenedor de applets que cargue un applet específico, y define el tamaño del área de visualización del applet (su anchura y altura en píxeles) en el contenedor de applets. • Por lo general, un applet y su correspondiente documento HTML se guardan en el mismo directorio. • Comúnmente, un explorador Web carga un documento HTML de una computadora (distinta a la del lector) conectada a Internet. • Cuando un contenedor de applets encuentra un documento HTML que contiene un applet, carga de manera automática el (los) archivo(s) .class del applet desde el mismo directorio en la computadora en la que reside el documento HTML. • El appletviewer sólo comprende las etiquetas y de HTML, e ignora a todas las demás etiquetas en el documento. • El appletviewer es un sitio ideal para probar un applet y asegurarse de que se ejecute apropiadamente. Una vez que se verifica la ejecución del applet, se pueden agregar sus etiquetas HTML a una página Web que otros puedan ver en sus exploradores Web.

Sección 20.4 Métodos del ciclo de vida de los applets • Hay cinco métodos de applet que el contenedor de applets llama, desde el momento en el que se carga el applet en el explorador Web, hasta el momento en que el explorador Web termina el applet. Estos métodos corresponden a varios aspectos del ciclo de vida de un applet. • El contenedor de applets llama una vez al método init, cuando se carga un applet para ejecutarlo. Este método inicializa el applet. • El contenedor de applets llama al método start una vez que el método init termina de ejecutarse. Además, si el usuario navega hacia otro sitio Web y después regresa a la página HTML del applet, se llama otra vez al método start. • El contenedor de applets llama al método paint después de los métodos init y start. El método paint también se llama cuando el applet necesita volver a dibujarse. • El contenedor de applets llama al método stop cuando el usuario sale de la página Web del applet, al navegar hacia otra página Web. • El contenedor de applets llama al método destroy cuando el applet se va a eliminar de la memoria. Esto ocurre cuando el usuario sale de la sesión de navegación, al cerrar todas las ventanas del explorador Web, y también puede ocurrir a discreción del explorador Web, cuando el usuario navega hacia otras páginas Web.

www.elsolucionario.net

856

Capítulo 20 Introducción a los applets de Java

Terminología .htm, extensión de archivo .html, extensión de archivo , etiqueta altura (height) de un applet anchura (width) de un applet

applet appletviewer

atributo contenedor de applets demo, directorio del JDK elemento de HTML elemento de HTML de un applet etiqueta

init, método de JApplet JApplet, clase

Lenguaje de marcado de hipertexto (HTML) paint, método de JApplet parseDouble, método de Double Salir, elemento de menú en el appletviewer signo < signo > start, método de JApplet stop, método de JApplet Subprograma, menú en el appletviewer Volver a cargar, elemento de menú en el appletviewer

Ejercicio de autoevaluación 20.1

Complete las siguientes oraciones: a) Los applets de Java empiezan a ejecutarse con una serie de llamadas a tres métodos: _________________, _________________ y _________________. b) El método _________________ se invoca para un applet cada vez que el usuario de un explorador Web sale de la página HTML en la que reside el applet. c) Todo applet debe extender a la clase _________________. d) El _________________ o un explorador Web pueden utilizarse para ejecutar un applet de Java. e) El método _________________ se llama cada vez que el usuario de un explorador Web vuelve a visitar la página HTML en la que reside un applet. f ) Para cargar un applet en un explorador Web, debe primero definir un archivo _________________. g) El método _________________ se llama una vez cuando el applet empieza a ejecutarse. h) El método _________________ se invoca para dibujar en un applet. i) El método _________________ se invoca para un applet cuando el explorador Web lo elimina de la memoria. j) Las etiquetas _________________ y _________________ de HTML especifican que debe cargarse un applet en un contenedor de applets, y ejecutarse.

Respuestas a los ejercicios de autoevaluación 20.1 a) init, start, paint. b) stop. c) JApplet (o Applet). d) appletviewer. e) start. f ) HTML. g) init. h) paint. i) destroy. j) , .

Ejercicios 20.2 Escriba un applet que pida al usuario que introduzca dos números de punto flotante, que obtenga los dos números del usuario y dibuje su suma, producto (multiplicación), diferencia y cociente (división). Use las técnicas que se muestran en la figura 20.10. 20.3 Escriba un applet que pida al usuario que introduzca dos números de punto flotante, que obtenga los números del usuario y muestre los dos números primero, y después el número más grande seguido de las palabras "es mayor que" como una cadena en el applet. Si los números son iguales, el applet deberá imprimir el mensaje "Estos numeros son iguales". Use las técnicas que se muestran en la figura 20.10. 20.4 Escriba un applet que reciba tres números de punto flotante del usuario y que muestre la suma, el promedio, el producto, el menor y el mayor de estos números, como cadenas en el applet. Use las técnicas que se muestran en la figura 20.10. 20.5 Escriba un applet que pida al usuario que introduzca el radio de un círculo como un número de punto flotante, y que dibuje el diámetro, circunferencia y área del círculo. Use el valor 3.14159 para π. Use las técnicas que se muestran en la figura 20.10. [Nota: también puede usar la constante predefinida Math.PI para el valor de π. Esta constante es

www.elsolucionario.net

Ejercicios

857

más precisa que el valor 3.14159. La clase Math se define en el paquete java.lang, por lo que no necesita importarla]. Use las siguientes fórmulas (r es el radio): diámetro = 2r circunferencia = 2πr área = πr 2 20.6 Escriba un applet que lea cinco enteros, determine cuáles son el mayor y el menor en el grupo, y que los imprima. Use sólo las técnicas de programación que aprendió en este capítulo y en el capítulo 2. Dibuje los resultados en el applet. 20.7

Escriba un applet que dibuje un patrón de tablero de damas, como se muestra a continuación:

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

20.8

Escriba un applet que dibuje rectángulos de distintos tamaños y posiciones.

20.9

Escriba un applet que permita al usuario introducir valores para los argumentos requeridos por el método y que después dibuje un rectángulo usando los cuatro valores de entrada.

drawRect,

20.10 La clase Graphics contiene el método drawOval, el cual recibe como argumentos los mismos cuatro argumentos que el método drawRect. Los argumentos para el método drawOval especifican el “cuadro delimitador” para el óvalo; los lados del cuadro delimitador son los límites del óvalo. Escriba un applet en Java que dibuje un óvalo y un rectángulo con los mismos cuatro argumentos. El óvalo debe tocar el rectángulo en el centro de cada lado. 20.11 Modifique la solución al ejercicio 20.10 para imprimir óvalos de distintas formas y tamaños. 20.12 Escriba un applet que permita al usuario introducir los cuatro argumentos requeridos por el método drawOval, y que después dibuje un óvalo usando los cuatro valores de entrada.

www.elsolucionario.net

21 Multimedia: applets y aplicaciones

La rueda que rechina más fuerte... es la que requiere la grasa. —John Billings (Henry Wheeler Shaw)

Utilizaremos una señal que he probado que tiene gran alcance y es fácil de gritar. ¡Yajuuuu!

OBJETIVOS En este capítulo aprenderá a:

—Zane Grey

Q

Obtener, mostrar y escalar imágenes.

Q

Crear animaciones a partir de secuencias de imágenes.

Q

Crear mapas de imágenes.

Q

Obtener sonidos, reproducirlos, hacer que se reproduzcan indefinidamente y detenerlos mediante el uso de un objeto AudioClip.

Q

Reproducir video mediante la interfaz Player.

Hay un movimiento de vaivén natural en un pez dorado. —Walt Disney

Entre el movimiento y el acto cae la sombra. —Thomas Stearns Eliot

www.elsolucionario.net

Pla n g e ne r a l

21.1 Introducción

21.1 21.2 21.3 21.4 21.5 21.6 21.7 21.8

859

Introducción Cómo cargar, mostrar y escalar imágenes Animación de una serie de imágenes Mapas de imágenes Carga y reproducción de clips de audio Reproducción de video y otros medios con el Marco de trabajo de medios de Java Conclusión Recursos Web Resumen | Terminología | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios Sección especial: proyectos de multimedia retadores

21.1 Introducción Bienvenido a lo que podría ser la mayor revolución en la historia de la industria computacional. Todos los que entramos al campo hace décadas, estábamos interesados en usar las computadoras principalmente para realizar cálculos aritméticos a grandes velocidades. A medida que el campo de la computación fue evolucionando, empezamos a darnos cuenta de que las herramientas de manipulación de datos de las computadoras son igualmente importantes. El “atractivo” de Java es multimedia (el uso de sonido, imágenes, gráficos y video para hacer que las aplicaciones “cobren vida”). Aunque la mayor parte del contenido multimedia en Java es bidimensional, los programadores de Java ya pueden utilizar la API Java 3D para crear aplicaciones importantes con gráficos en 3D (Sun ofrece un tutorial en línea para la API Java 3D en java.sun.com/developer/onlineTraining/ java3d).

La programación de multimedia ofrece muchos nuevos retos. El campo es de por sí enorme, y crece rápidamente. La mayoría de las nuevas computadoras que se venden actualmente están “listas para multimedia”, con unidades de CD-RW o DVD, tarjetas de audio e incluso con herramientas especiales de video. Las computadoras de escritorio y portátiles económicas son tan poderosas que pueden almacenar y reproducir video y sonido con calidad de DVD, y esperamos ver aún más avances en los tipos de herramientas de multimedia programables, disponibles a través de los lenguajes de programación. Algo que hemos aprendido es a planear para “lo imposible”; en los campos de la computación y las comunicaciones, lo “imposible” se ha vuelto realidad en repetidas ocasiones. Entre los usuarios que desean gráficos, muchos ahora desean gráficos de color tridimensionales, de alta resolución. El despliegue de imágenes tridimensionales verdaderas tal vez esté disponible dentro de la siguiente década. Imagine tener una televisión tridimensional, de alta resolución tipo cine. ¡Los eventos deportivos y de entretenimiento parecerán estar llevándose a cabo en la sala de su casa! Los estudiantes de medicina en todo el mundo verán cómo se realizan operaciones a miles de millas de distancia, como si estuvieran ocurriendo en la misma habitación. Las personas aprenderán a conducir con simuladores de conducción extremadamente realistas en sus hogares antes de ponerse al volante. Las posibilidades son emocionantes e interminables. La multimedia demanda un poder computacional extraordinario. Hasta hace poco, no había computadoras a precios accesibles con ese tipo de poder. Los procesadores ultrarrápidos de la actualidad hacen posible la multimedia con efectividad. Las industrias de la computación y la comunicación serán los principales beneficiarios de la revolución multimedia. Los usuarios estarán dispuestos a pagar por procesadores más veloces, memorias más grandes y anchos de banda de comunicación más amplios, que soporten las exigencias de las aplicaciones multimedia. Irónicamente, los usuarios tal vez no tengan que pagar más, ya que la feroz competencia en estas industrias hace que los precios bajen. Necesitamos lenguajes de programación que faciliten la creación de aplicaciones multimedia. La mayoría de los lenguajes de programación no tienen herramientas multimedia integradas . Sin embargo, a través de sus bibliotecas de clases, Java proporciona extensas características multimedia que le permitirán empezar a desarrollar poderosas aplicaciones multimedia inmediatamente. En este capítulo presentamos varios ejemplos de las interesantes características multimedia que usted necesitará para crear útiles aplicaciones, incluyendo las siguientes: 1. Los fundamentos de la manipulación de imágenes.

www.elsolucionario.net

860

Capítulo 21

Multimedia: applets y aplicaciones

2. Creación de animaciones refinadas. 3. Reproducción de archivos de audio mediante la interfaz AudioClip. 4. Creación de mapas de imágenes que pueden detectar cuando el cursor se encuentra sobre ellos, incluso sin hacer clic con el ratón. 5. Reproducción de archivos de video mediante el uso de la interfaz Player. Los ejercicios para este capítulo sugieren docenas de proyectos retadores e interesantes. Cuando creamos estos ejercicios, las ideas no dejaban de surgir. La multimedia impulsa la creatividad en formas que no hemos experimentado con las herramientas “convencionales” de computación. [Nota: las herramientas de multimedia de Java van más allá de las que se presentan en este capítulo. Entre estas herramientas se incluyen la API del marco de trabajo de medios de Java (JMF, Java Media Framework API) para agregar medios de audio y video a una aplicación, la API de sonido de Java (Java Sound API) para reproducir, grabar y modificar audio; la API 3D de Java (Java 3D API) para crear y modificar gráficos en 3D; la API de procesamiento avanzado de imágenes de Java (Java Advanced Imaging API) para las herramientas de procesamiento de imágenes, como recortar y escalar; la API de síntesis de voz de Java (Java Speech API) para introducir comandos del usuario, o emitir comandos de voz al usuario; la API 2D de Java (Java 2D API) para crear y modificar gráficos en 2D, la cual cubrimos en el capítulo 12; y la API de E/S de imágenes de Java (Java Image I/O API) para leer y escribir imágenes en archivos. En la sección 21.8 se proporcionan vínculos Web para cada una de estas APIs].

21.2 Cómo cargar, mostrar y escalar imágenes Las herramientas multimedia de Java incluyen gráficos, imágenes, animaciones, sonidos y video. Empezaremos nuestra discusión con las imágenes, y emplearemos distintas en este capítulo. Los desarrolladores pueden crear dichas imágenes con cualquier software para manipulación de imágenes, como Adobe® PhotoshopTM, Jasc® Paint Shop ProTM o Microsoft® Paint. El applet de la figura 21.1 demuestra cómo cargar un objeto Image (paquete java.awt) y cómo cargar un objeto ImageIcon (paquete javax.swing). Ambas clases se utilizan para cargar y mostrar imágenes en pantalla. El applet muestra al objeto Image en su tamaño original y escalado a un tamaño mayor, utilizando dos versiones del método drawImage de Graphics. También dibuja el objeto ImageIcon, utilizando el método paintIcon del icono. La clase ImageIcon implementa a la interfaz Serializable, la cual permite escribir fácilmente obje-

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

// Fig. 21.1: CargarImagenYEscalar.java // Carga una imagen y la muestra en su tamaño original y al doble de su // tamaño original. Carga y muestra la misma imagen como un objeto ImageIcon. import java.awt.Graphics; import java.awt.Image; import javax.swing.ImageIcon; import javax.swing.JApplet; public class CargarImagenYEscalar extends JApplet { private Image imagen1; // crea un objeto Image private ImageIcon imagen2; // crea un objeto ImageIcon // carga la imagen cuando se carga el applet public void init() { imagen1 = getImage( getDocumentBase(), "floresrojas.png" ); imagen2 = new ImageIcon( "floresamarillas.png" ); } // fin del método init // muestra la imagen public void paint( Graphics g )

Figura 21.1 | Cómo cargar y mostrar una imagen en un applet. (Parte 1 de 2).

www.elsolucionario.net

21.2

23 24 25 26 27 28 29 30 31 32 33 34

Cómo cargar, mostrar y escalar imágenes

861

{ super.paint( g ); g.drawImage( imagen1, 0, 0, this ); // dibuja la imagen original // dibuja la imagen para que se ajuste a la anchura y altura menos 120 píxeles g.drawImage( imagen1, 0, 120, getWidth(), getHeight() - 120, this ); // dibuja un icono, usando su método paintIcon imagen2.paintIcon( this, g, 180, 0 ); } // fin del método paint } // fin de la clase CargarImagenYEscalar

Figura 21.1 | Cómo cargar y mostrar una imagen en un applet. (Parte 2 de 2).

tos ImageIcon en un archivo, o enviarlos a través de Internet. La clase ImageIcon es también más fácil de usar que Image, ya que su constructor puede recibir argumentos de varios formatos distintos, incluyendo un arreglo byte que contiene los bytes de una imagen, un objeto Image ya cargado en memoria, y un objeto String o URL, los cuales pueden usarse para representar la ubicación de la imagen. Un objeto URL representa a un Localizador uniforme de recursos, el cual sirve como apuntador a un recurso en World Wide Web, en su computadora o en cualquier equipo conectado en red. Un objeto URL se utiliza con más frecuencia cuando se accede a los datos en el equipo actual. Al utilizar un objeto URL, el programador también puede acceder a la información en los sitios Web, como cuando se busca información en una base de datos o a través de un motor de búsqueda. En las líneas 11 y 12 se declaran variables Image e ImageIcon, respectivamente. La clase Image es una clase abstract; por lo tanto, el applet no puede crear un objeto de la clase Image directamente. En vez de ello, debe llamar a un método que haga que el contenedor de applets cargue y devuelva el objeto Image para usarlo en el programa. La clase Applet (la superclase directa de JApplet) proporciona el método getImage (línea 17 en el método init) para cargar un objeto Image en un applet. Esta versión de getImage recibe dos argumentos: la ubicación del archivo de la imagen y el nombre de ese archivo. En el primer argumento, el método getDocumentBase de Applet devuelve un objeto URL que representa la ubicación de la imagen en Internet (o en su computadora, si el applet se cargó desde ahí). El método getDocumentBase devuelve la ubicación del archivo HTML como un objeto de la clase URL. En conjunto, los dos argumentos especifican el nombre único y la ruta del archivo que se va a cargar (en este caso, el archivo floresrojas.png almacenado en el mismo directorio que el archivo HTML que invocó al applet). El segundo argumento especifica el nombre de un archivo de imagen. Java

www.elsolucionario.net

862

Capítulo 21

Multimedia: applets y aplicaciones

soporta varios formatos de imágenes, incluyendo GIF (Formato de Intercambio de Gráficos), JPEG (Grupo Unido de Expertos en Fotografía) y PNG (Gráficos Portables de Red). Los nombres de archivo para cada uno de estos tipos terminan con .gif, .jpg (o .jpeg) y .png, respectivamente.

Tip de portabilidad 21.1 La clase Image es una clase abstract; como resultado, los programas no pueden instanciar la clase Image para crear objetos. Para lograr una independencia de plataformas, la implementación de Java en cada plataforma proporciona su propia subclase de Image para almacenar información sobre las imágenes. Los métodos que devuelven referencias a objetos Image en realidad devuelven referencias a objetos de la subclase Image de la implementación de Java.1

En la línea 17 comienza a cargarse la imagen del equipo local (o comienza a descargarse la imagen de Internet). Cuando la imagen es requerida por el programa, se carga en un subproceso de ejecución separado. Recuerde que un subproceso es una actividad en paralelo, y que hablaremos sobre los subprocesos con detalle en el capítulo 23, Subprocesamiento múltiple. Al utilizar un subproceso separado para cargar una imagen, el programa puede continuar su ejecución mientras la imagen se carga. [Nota: si el archivo solicitado no está disponible, el método getImage no lanza una excepción. Se devuelve un objeto Image, pero cuando éste se muestra en pantalla mediante el método drawImage, no aparece nada]. La clase ImageIcon no es una clase abstract; por lo tanto, un programa puede crear un objeto ImageIcon. La línea 18 en el método init crea un objeto ImageIcon que carga la imagen floresamarillas.png. La clase ImageIcon proporciona varios constructores que permiten a los programas inicializar objetos ImageIcon con imágenes del equipo local, o con imágenes almacenadas en Internet. El método paint del applet (líneas 22 a 33) muestra las imágenes. En la línea 26 se utiliza el método drawImage de Graphics para mostrar un objeto Image. El método drawImage recibe cuatro argumentos. El primer argumento es una referencia al objeto Image que se va a mostrar (imagen1). Los argumentos segundo y tercero son las coordenadas x y y en las que se mostrará la imagen en el applet; las coordenadas indican la ubicación de la esquina superior izquierda de la imagen. El último argumento es una referencia a un objeto ImageObserver; una interfaz implementada por la clase Component. Como la clase JApplet extiende a Component de manera indirecta, todos los objetos JApplet son objetos ImageObserver. Este argumento es importante cuando se muestran imágenes extensas que requieren de mucho tiempo para descargarse desde Internet. Es posible que un programa intente mostrar la imagen antes de que ésta se descargue completamente. El objeto ImageObserver es notificado para actualizar la imagen que se mostrará, a medida que el objeto Image se carga y actualiza la imagen en la pantalla, si ésta no estaba completa cuando se empezó a mostrar. Al ejecutar este applet, observe cuidadosamente cómo se van mostrando piezas de la imagen mientras ésta se carga. [Nota: en equipos rápidos, tal vez no pueda percatarse de este efecto]. En la línea 29 se utiliza una versión sobrecargada del método drawImage para mostrar una versión escalada de la imagen. Los argumentos cuarto y quinto especifican la anchura y altura de la imagen para mostrarla en pantalla. El método drawImage escala la imagen para que se ajuste a la anchura y altura especificadas. En este ejemplo, el cuarto argumento indica que la anchura de la imagen escalada debe ser igual a la anchura del applet, y el quinto argumento indica que la altura debe ser 120 píxeles menor que la altura del applet. La anchura y la altura del applet se determinan mediante llamadas a los métodos getWidth y getHeight (heredados de la clase Component). En la línea 32 se utiliza el método paintIcon de ImageIcon para mostrar la imagen. Este método requiere cuatro argumentos: una referencia al objeto Component en el que se mostrará la imagen, una referencia al objeto Graphics que desplegará la imagen, la coordenada x de la esquina superior izquierda de la imagen y la coordenada y de la esquina superior izquierda de la imagen.

21.3 Animación de una serie de imágenes El siguiente ejemplo demuestra cómo animar una serie de imágenes almacenadas en un arreglo de objetos ImageIcon. La animación presentada en las figuras 21.2 y 21.3 se implementa mediante el uso de una subclase de JPanel llamada AnimadorLogoJPanel (figura 21.2) que puede adjuntarse a una ventana de aplicación o a un objeto JApplet. La clase AnimadorLogo (figura 21.3) también declara un método main (líneas 8 a 20 de la figura 21.3) para ejecutar la animación como una aplicación. El método main declara una instancia de la clase JFrame y adjunta un objeto AnimadorLogoJPanel al objeto JFrame para mostrar la animación.

www.elsolucionario.net

21.3

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

Animación de una serie de imágenes

863

// Fig. 21.2: AnimadorLogoJPanel.java // Animación de una serie de imágenes. import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.Graphics; import javax.swing.ImageIcon; import javax.swing.JPanel; import javax.swing.Timer; public class AnimadorLogoJPanel extends JPanel { private final static String NOMBRE_IMAGEN = "deitel"; // nombre de la imagen base protected ImageIcon imagenes[]; // arreglo de imágenes private final int TOTAL_IMAGENES = 30; // número de imágenes private int imagenActual = 0; // índice de la imagen actual private final int RETRASO_ANIMACION = 50; // retraso en milisegundos private int anchura; // anchura de la imagen private int altura; // altura de la imagen private Timer temporizadorAnimacion; // objeto Timer que controla la animación // el constructor inicializa el objeto AnimadorLogoJPanel, cargando las imágenes public AnimadorLogoJPanel() { imagenes = new ImageIcon[ TOTAL_IMAGENES ]; // carga 30 imágenes for ( int cuenta = 0; cuenta < imagenes.length; cuenta++ ) imagenes[ cuenta ] = new ImageIcon( getClass().getResource( "imagenes/" + NOMBRE_IMAGEN + cuenta + ".gif" ) ); // este ejemplo supone que todas las imágenes tienen la misma anchura y altura anchura = imagenes[ 0 ].getIconWidth(); // obtiene la anchura del icono altura = imagenes[ 0 ].getIconHeight(); // obtiene la altura del icono } // fin del constructor de AnimadorLogoJPanel // muestra la imagen actual public void paintComponent( Graphics g ) { super.paintComponent( g ); // llama al método paintComponent de la superclase imagenes[ imagenActual ].paintIcon( this, g, 0, 0 ); // establece la siguiente imagen a dibujar, sólo si el temporizador está funcionando if ( temporizadorAnimacion.isRunning() ) imagenActual = ( imagenActual + 1 ) % TOTAL_IMAGENES; } // fin del método paintComponent // inicia la animación, o la reinicia si se vuelve a mostrar la ventana public void iniciarAnimacion() { if ( temporizadorAnimacion == null ) { imagenActual = 0; // muestra la primera imagen // crea temporizador temporizadorAnimacion = new Timer( RETRASO_ANIMACION, new ManejadorTimer() );

Figura 21.2 | Animación de una serie de imágenes. (Parte 1 de 2).

www.elsolucionario.net

864

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97

Capítulo 21

Multimedia: applets y aplicaciones

temporizadorAnimacion.start(); // inicia el objeto Timer } // fin de if else // temporizadorAnimacion ya existe; reinicia la animación { if ( ! temporizadorAnimacion.isRunning() ) temporizadorAnimacion.restart(); } // fin de else } // fin del método iniciarAnimacion // detiene el temporizador de la animación public void detenerAnimacion() { temporizadorAnimacion.stop(); } // fin del método detenerAnimacion // devuelve el tamaño mínimo de la animación public Dimension getMinimumSize() { return getPreferredSize(); } // fin del método getMinimumSize // devuelve el tamaño preferido de la animación public Dimension getPreferredSize() { return new Dimension( anchura, altura ); } // fin del método getPreferredSize // clase interna para manejar los eventos de acción del objeto Timer private class ManejadorTimer implements ActionListener { // responde a un evento del objeto Timer public void actionPerformed( ActionEvent actionEvent ) { repaint(); // vuelve a dibujar la animación } // fin del método actionPerformed } // fin de la clase ManejadorTimer } // fin de la clase AnimadorLogoJPanel

Figura 21.2 | Animación de una serie de imágenes. (Parte 2 de 2).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

// Fig. 21.3: AnimadorLogo.java // Animación de una serie de imágenes. import javax.swing.JFrame; public class AnimadorLogo { // ejecuta la animación en un objeto JFrame public static void main( String args[] ) { AnimadorLogoJPanel animacion = new AnimadorLogoJPanel(); JFrame ventana = new JFrame( "Prueba de Animador" ); // establece la ventana ventana.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); ventana.add( animacion ); // agrega el panel al marco ventana.pack();

// hace la ventana lo suficientemente grande para su GUI

Figura 21.3 | Visualización de imágenes en un objeto JFrame. (Parte 1 de 2).

www.elsolucionario.net

21.3

17 18 19 20 21

ventana.setVisible( true ); animacion.iniciarAnimacion(); } // fin de main } // fin de la clase AnimadorLogo

Animación de una serie de imágenes

865

// muestra la ventana // inicia la animación

Figura 21.3 | Visualización de imágenes en un objeto JFrame. (Parte 2 de 2). La clase AnimadorLogoJPanel (figura 21.2) mantiene un arreglo de objetos ImageIcon que se cargan en el constructor (líneas 24 a 36). En las líneas 29 a 31 se crea cada uno de los objetos ImageIcon y se cargan las 30 imágenes de la animación en el arreglo imagenes. El argumento del constructor utiliza la concatenación de cadenas para construir el nombre del archivo a partir de las piezas "imagenes/", NOMBRE_IMAGEN, cuenta y ".gif ". Cada una de las imágenes de la animación se encuentra en un archivo llamado deitel#.gif, en donde # es un valor en el rango de 0 a 29, el cual se especifica mediante la variable de control de ciclo cuenta. En las líneas 34 y 35 se determinan la anchura y altura de la animación, con base en el tamaño de la primera imagen del arreglo imágenes; suponemos que todas las imágenes tienen la misma anchura y altura. Una vez que el constructor de AnimadorLogoJPanel carga las imágenes, el método main de la figura 21.3 establece la ventana en la que aparecerá la animación (líneas 12 a 17), y en la línea 19 se hace una llamada al método iniciarAnimacion de AnimadorLogoJPanel (declarado en las líneas 51 a 68 de la figura 21.2). Este método inicia la animación del programa por primera vez, o reinicia la animación que el programa detuvo anteriormente. [Nota: este método se llama cuando el programa se ejecuta por primera vez, para iniciar la animación. Aunque proporcionamos la funcionalidad para que este método reinicie la animación si se ha detenido, el ejemplo no llama al método para este propósito. Sin embargo, agregamos la funcionalidad en caso de que usted optara por agregar componentes de GUI que permitan al usuario iniciar y detener la animación]. Por ejemplo, para hacer que una animación sea “amigable para el usuario” en un applet, debe detenerse cuando el usuario cambie de páginas Web. Si el usuario regresa a la página Web con la animación, se puede llamar al método iniciar Animacion para reiniciar la animación. La animación es controlada por una instancia de la clase Timer (del paquete javax.swing). Un objeto Timer genera eventos ActionEvent en un intervalo fijo en milisegundos (que generalmente se especifica como argumento para el constructor de Timer) y notifica a todos sus objetos ActionListener cada vez que ocurre un evento ActionEvent. En la línea 53 se determina si la referencia Timer llamada temporizadorAnimacion es null. De ser así, se está llamando al método iniciarAnimacion por primera vez, y hay que crear un objeto Timer para que la animación pueda empezar. En la línea 55 se establece imagenActual en 0, lo cual indica que la animación debe empezar con la imagen que se encuentra en el primer elemento del arreglo imagenes. En las líneas 58 y 59 se asigna un nuevo objeto Timer a temporizadorAnimacion. El constructor de Timer recibe dos argumentos: el retraso en milisegundos (RETRASO_ANIMACION es 50, según lo especificado en la línea 17) y el objeto ActionListener que responderá a los objetos ActionEvent de Timer. Para el segundo argumento, se crea un objeto de la clase ManejadorTimer. Esta clase, que implementa a ActionListener, se declara en las líneas 89 a 96. En la línea 61 se inicia el objeto Timer. Una vez iniciado, temporizadorAnimacion generará un evento ActionEvent cada 50 milisegundos. Cada vez que se genera un evento ActionEvent, se hace una llamada al manejador de eventos actionPerformed de Timer (líneas 92 a 95). En la línea 94 se hace una llamada al método repaint de AnimadorLogoJPanel para programar una llamada al método paintComponent de AnimadorLogoJPanel (líneas 39 a 48). Recuerde que cualquier subclase de JComponent que dibuje, debe hacerlo en su método paintComponent. En el capítulo 11 vimos que la primera instrucción en cualquier método paintComponent debe ser una llamada al método paintComponent de la superclase, para asegurar que los componentes Swing se visualicen en forma correcta. Si la animación empezó antes, entonces se creó nuestro objeto Timer y la condición en la línea 53 se evalúa como false. El programa continúa con las líneas 65 y 66, en donde se reinicia la animación que el programa

www.elsolucionario.net

866

Capítulo 21

Multimedia: applets y aplicaciones

detuvo anteriormente. La condición if en la línea 65 utiliza el método isRunning de Timer para determinar si el objeto Timer está funcionando (es decir, si genera eventos). Si no está funcionando, en la línea 66 se hace una llamada al método restart de Timer para indicar que el objeto Timer debe empezar a generar eventos otra vez. Una vez que esto ocurre, se vuelve a llamar al método actionPerformed (el manejador de eventos del objeto Timer) en intervalos regulares. Cada vez, se realiza una llamada al método repaint (línea 94), lo cual provoca que se haga una llamada al método paintComponent y se muestra la siguiente imagen. En la línea 43 se pinta el objeto ImageIcon almacenado en el elemento imagenActual del arreglo. En las líneas 46 y 47 se determina si el objeto temporizadorAnimacion está funcionando y, de ser así, se prepara la siguiente imagen a mostrar en pantalla, incrementando el valor de imagenActual en 1. El cálculo del residuo asegura que el valor de imagenActual esté en 0 (para repetir la secuencia de animación) cuando se incremente más allá de 29 (el índice del último elemento en el arreglo). La instrucción if asegura que, si se hace una llamada a paintComponent mientras el objeto Timer está detenido, se mostrará la misma imagen. Esto podría ser útil si se proporciona una GUI que permita al usuario iniciar y detener la animación. Por ejemplo, si la animación se detiene y el usuario la cubre con otra ventana, y después la descubre, se realizará una llamada al método paintComponent. En este caso, no queremos que la animación muestre la siguiente imagen (ya que la animación se ha detenido). Simplemente queremos que la ventana muestre la misma imagen hasta que se reinicie la animación. El método detenerAnimacion (líneas 71 a 74) detiene la animación, llamando al método stop de Timer para indicar que el objeto Timer debe dejar de generar eventos. Esto evita que actionPerformed llame a repaint para iniciar el proceso de pintar la siguiente imagen en el arreglo. [Nota: al igual que para reiniciar la animación, este ejemplo define pero no utiliza el método detenerAnimacion. Hemos proporcionado este método para fines de demostración, o si el usuario desea modificar este ejemplo, de manera que pueda detener y reiniciar la animación].

Observación de ingeniería de software 21.1 Al crear una animación para utilizarla en un applet, debe proporcionar un mecanismo para deshabilitar la animación cuando el usuario navega en una nueva página Web, distinta de la página en la que reside el applet de la animación.

Recuerde que, al extender la clase JPanel, estamos creando un nuevo componente de GUI. Por ende, debemos asegurar que funcione igual que otros componentes para fines de usarlo en un esquema. Los administradores de esquemas a menudo utilizan el método getPreferredSize de un componente de GUI (heredado de la clase java.awt.Component) para determinar la anchura y altura preferidas del componente, al distribuirlo como parte de una GUI. Si un nuevo componente tiene una anchura y altura preferidas, debe sobrescribir el método getPreferredSize (líneas 83 a 86) para regresar esa anchura y altura como un objeto de la clase Dimension (paquete java.awt). La clase Dimension representa a la anchura y altura de un componente de GUI. En este ejemplo, las imágenes tienen 160 píxeles de anchura y 80 píxeles de largo, por lo que el método getPreferredSize devuelve un objeto Dimension que contiene los números 160 y 80 (determinados en las líneas 34 y 35). Archivo Nuevo Abrir... Cerrar

Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 21.1 El tamaño predeterminado de un objeto JPanel es de 10 píxeles de ancho y 10 píxeles de alto.

Observación de apariencia visual 21.2 Al crear una subclase de JPanel (o de cualquier otra clase derivada de JComponent), sobrescriba el método getPresi el nuevo componente debe tener una anchura y altura preferidas específicas.

ferredSize

En las líneas 77 a 80 se sobrescribe el método getMinimumSize. Este método determina la anchura y altura mínimas del componente. Al igual que con el método getPreferredSize, los nuevos componentes deben sobrescribir el método getMinimumSize (también heredado de la clase Component). El método getMinimumSize simplemente llama a getPreferredSize (una práctica común de programación) para indicar que los tamaños mínimo y preferido son iguales. Algunos administradores de esquemas ignoran las dimensiones especificadas por estos métodos. Por ejemplo, las regiones NORTH y SOUTH de un objeto BorderLayout utilizan solamente la altura preferida del componente.

www.elsolucionario.net

21.4

Archivo Nuevo Abrir... Cerrar

Archivo Nuevo Abrir... Cerrar

Mapas de imágenes

867

Observación de apariencia visual 21.3 Si un nuevo componente de GUI tiene una anchura y altura mínimas (es decir, medidas más pequeñas harían que el componente se desplegara en forma ineficiente), sobrescriba el método getMinimumSize para regresar la anchura y altura mínimas como una instancia de la clase Dimension.

Observación de apariencia visual 21.4 Para muchos componentes de GUI se implementa el método llamada al método getPreferredSize del componente.

getMinimumSize

para devolver el resultado de una

21.4 Mapas de imágenes Los mapas de imágenes son una técnica común utilizada para crear páginas Web interactivas. Un mapa de imágenes es una imagen con áreas activas en las que el usuario puede hacer clic para realizar una tarea, como cargar una página Web distinta en un explorador Web. Cuando el usuario posiciona el puntero del ratón sobre un área activa, generalmente aparece un mensaje descriptivo en el área de estado del explorador Web, o en un cuadro de información sobre herramientas. En la figura 21.4 se carga una imagen que contiene varios de los íconos de tips comunes que se utilizan en este libro. El programa permite al usuario posicionar el puntero del ratón sobre un icono y mostrar un mensaje descriptivo asociado con el icono. El manejador de eventos mouseMoved (líneas 39 a 43) toma las coordenadas del ratón y las pasa al método traducirPosicion (líneas 58 a 69). El método traducirPosicion evalúa las coordenadas para determinar sobre cuál icono se posicionó el ratón cuando ocurrió el evento mouseMoved; después el método devuelve un mensaje indicando lo que el icono representa. Este mensaje se muestra en la barra de estado del contenedor de applets, mediante el uso del método showStatus de la clase Applet. Si hace clic en el applet de la figura 21.4, no se provocará ninguna acción. En el capítulo 24, Redes, hablamos sobre las técnicas requeridas para cargar otra página Web en un explorador Web mediante objetos URL y la interfaz AppletContext. Utilizando esas técnicas, este applet podría asociar cada icono con un objeto URL que el explorador Web mostraría cuando el usuario hiciera clic en el icono.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

// Fig. 21.4: MapaImagenes.java // Demostración de un mapa de imágenes. import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.awt.Graphics; import javax.swing.ImageIcon; import javax.swing.JApplet; public class MapaImagenes extends JApplet { private ImageIcon imagenMapa; private static final String leyendas[] = { "Error comun de programacion", "Buena practica de programacion", "Observacion de apariencia visual", "Tip de rendimiento", "Tip de portabilidad", "Observacion de ingenieria de software", "Tip para prevenir errores" }; // establece los componentes de escucha del ratón public void init() { addMouseListener( new MouseAdapter() // clase interna anónima { // indica cuando el puntero del ratón sale del área del applet

Figura 21.4 | Mapa de imágenes. (Parte 1 de 3).

www.elsolucionario.net

868

27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70

Capítulo 21

Multimedia: applets y aplicaciones

public void mouseExited( MouseEvent evento ) { showStatus( "Apuntador fuera de applet" ); } // fin del método mouseExited } // fin de la clase interna anónima ); // fin de la llamada a addMouseListener addMouseMotionListener( new MouseMotionAdapter() // clase interna anónima { // determina el icono sobre el cual aparece el ratón public void mouseMoved( MouseEvent evento ) { showStatus( traducirPosicion( evento.getX(), evento.getY() ) ); } // fin del método mouseMoved } // fin de la clase interna anónima ); // fin de la llamada a addMouseMotionListener imagenMapa = new ImageIcon( "iconos.png" ); // obtiene la imagen } // fin del método init // muestra imagenMapa public void paint( Graphics g ) { super.paint( g ); imagenMapa.paintIcon( this, g, 0, 0 ); } // fin del método paint // devuelve leyenda del tip correspondiente, con base en las coordenadas del ratón public String traducirPosicion( int x, int y ) { // si las coordenadas están fuera de la imagen, regresa de inmediato if ( x >= imagenMapa.getIconWidth() || y >= imagenMapa.getIconHeight() ) return ""; // determina el número del icono (0 - 6) double anchuraIcono = ( double ) imagenMapa.getIconWidth() / 7.0; int numeroIcono = ( int )( ( double ) x / anchuraIcono ); return leyendas[ numeroIcono ]; // devuelve la leyenda del icono apropiado } // fin del método traducirPosicion } // fin de la clase MapaImagenes

Figura 21.4 | Mapa de imágenes. (Parte 2 de 3).

www.elsolucionario.net

21.5 Carga y reproducción de clips de audio

869

Observación de ingenieria de software

Figura 21.4 | Mapa de imágenes. (Parte 3 de 3).

21.5 Carga y reproducción de clips de audio Los programas de Java pueden manipular y reproducir clips de audio. Los usuarios pueden capturar sus propios clips de audio, y hay muchos clips disponibles en productos de software y a través de Internet. Su sistema necesita estar equipado con hardware de audio (bocinas y una tarjeta de sonido) para poder reproducir los clips de audio. Java proporciona varios mecanismos para reproducir sonidos en un applet. Los dos métodos más simples son el método play de Applet y el método play de la interfaz AudioClip. Hay varias capacidades de audio adicionales disponibles en el Marco de trabajo de medios de Java (Java Media Framework) y en las APIs de sonido de Java (Java Sound APIs). Si desea reproducir un sonido una vez en un programa, el método play de Applet carga

www.elsolucionario.net

870

Capítulo 21

Multimedia: applets y aplicaciones

el sonido y lo reproduce una vez; el sonido se marca para la recolección de basura una vez que se reproduce. El método play de Applet tiene dos formas: public void play( URL ubicacion, String nombreArchivoSonido ); public void play( URL urlSonido );

La primera versión carga el clip de audio almacenado en el archivo nombreArchivoSonido que se encuentra en ubicacion y reproduce el sonido. El primer argumento es normalmente una llamada al método getDocumentBase o getCodeBase del applet. El método getDocumentBase devuelve la ubicación del archivo HTML que cargó el applet. (Si el applet se encuentra en un paquete, el método devuelve la ubicación del paquete o archivo JAR que contiene a ese paquete). El método getCodeBase indica la ubicación del archivo .class del applet. La segunda versión del método play toma un objeto URL que contiene la ubicación y el nombre de archivo del audio clip. La instrucción play( getDocumentBase(), "hi.au" );

carga el clip de audio que se encuentra en el archivo hi.au y reproduce el clip una vez. El motor de sonido que reproduce los clips de audio soporta varios formatos de archivo de audio, incluyendo los formatos con extensión .au (formato de archivo de audio de Sun), .wav (formato de archivo de onda de Windows), .aif o .aiff (formato de archivo AIFF de Macintosh) y .mid o .rmi (MIDI, Interfaz digital para instrumentos musicales). El JMF (Marco de medios de Java) y las APIs de sonido de Java soportan formatos adicionales. El programa de la figura 21.5 demuestra cómo cargar y reproducir un objeto AudioClip (paquete java. applet). Esta técnica es más flexible que el método play de Applet. Un applet puede utilizar un objeto AudioClip para almacenar audio que se use en repetidas ocasiones a lo largo de la ejecución de un programa. El método getAudioClip de Applet tiene dos formas que toman los mismos argumentos que el método play que se describió anteriormente. El método getAudioClip devuelve una referencia a un objeto AudioClip. Un objeto AudioClip tiene tres métodos: play, loop y stop. Como se mencionó antes, el método play reproduce el clip de audio una sola vez. El método loop reproduce continuamente el clip de audio en segundo plano. El método stop termina el clip de audio que se esté reproduciendo en ese momento. En el programa, cada uno de estos métodos se asocia con un botón en el applet. Las líneas 62 y 63 en el método init del applet utilizan a getAudioClip para cargar dos archivos de audio: un archivo de onda de Windows (welcome.wav) y un archivo de audio de Sun (hi.au). El usuario puede seleccionar cuál clip de audio reproducir en el objeto JComboBox llamado sonidoJComboBox. Observe que el método stop del applet se sobrescribe en las líneas 68 a 71. Cuando el usuario cambia de páginas Web, el contenedor de applets llama al método stop del applet. Esto permite al applet dejar de reproducir el clip de audio. De no ser así, el clip de audio continúa reproduciéndose en segundo plano; incluso aunque el applet no se muestre en el navegador. Esto no es necesariamente un problema, pero puede ser molesto para el usuario que el clip de audio se reproduzca en forma continua. El método stop se proporciona aquí como una conveniencia para el usuario.

1 2 3 4 5 6 7 8 9 10 11 12 13 14

// Fig. 21.5: CargarAudioYReproducir.java // Carga un clip de audio y lo reproduce. import java.applet.AudioClip; import java.awt.event.ItemListener; import java.awt.event.ItemEvent; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import java.awt.FlowLayout; import javax.swing.JApplet; import javax.swing.JButton; import javax.swing.JComboBox; public class CargarAudioYReproducir extends JApplet {

Figura 21.5 | Carga y reproducción de un objeto AudioClip. (Parte 1 de 3).

www.elsolucionario.net

21.5 Carga y reproducción de clips de audio

15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73

private AudioClip sonido1, sonido2, sonidoActual; private JButton reproducirJButton, continuoJButton, detenerJButton; private JComboBox sonidoJComboBox; // carga la imagen cuando el applet empieza a ejecutarse public void init() { setLayout( new FlowLayout() ); String opciones[] = { "Welcome", "Hi" }; sonidoJComboBox = new JComboBox( opciones ); // crea objeto JComboBox sonidoJComboBox.addItemListener( new ItemListener() // clase interna anónima { // detiene el sonido y lo cambia por la selección del usuario public void itemStateChanged( ItemEvent e ) { sonidoActual.stop(); sonidoActual = sonidoJComboBox.getSelectedIndex() == 0 ? sonido1 : sonido2; } // fin del método itemStateChanged } // fin de la clase interna anónima ); // fin de la llamada al método addItemListener add( sonidoJComboBox ); // agrega objeto JComboBox al applet // establece el manejador de eventos de botón y los botones ManejadorBoton manejador = new ManejadorBoton(); // crea objeto JButton Reproducir reproducirJButton = new JButton( "Reproducir" ); reproducirJButton.addActionListener( manejador ); add( reproducirJButton ); // crea objeto JButton Continuo continuoJButton = new JButton( "Continuo" ); continuoJButton.addActionListener( manejador ); add( continuoJButton ); // crea JButton Detener detenerJButton = new JButton( "Stop" ); detenerJButton.addActionListener( manejador ); add( detenerJButton ); // carga los sonidos y establecet sonidoActual sonido1 = getAudioClip( getDocumentBase(), "welcome.wav" ); sonido2 = getAudioClip( getDocumentBase(), "hi.au" ); sonidoActual = sonido1; } // fin del método init // detiene el sonido cuando el usuario cambia a otra página Web public void stop() { sonidoActual.stop(); // detener AudioClip } // fin del método stop // clase interna privada para manejar eventos de botón

Figura 21.5 | Carga y reproducción de un objeto AudioClip. (Parte 2 de 3).

www.elsolucionario.net

871

872

74 75 76 77 78 79 80 81 82 83 84 85 86 87

Capítulo 21

Multimedia: applets y aplicaciones

private class ManejadorBoton implements ActionListener { // procesa los eventos de los botones reproducir, continuo y detener public void actionPerformed( ActionEvent actionEvent ) { if ( actionEvent.getSource() == reproducirJButton ) sonidoActual.play(); // reproducir AudioClip una vez else if ( actionEvent.getSource() == continuoJButton ) sonidoActual.loop(); // reproducir AudioClip en forma continua else if ( actionEvent.getSource() == detenerJButton ) sonidoActual.stop(); // detener AudioClip } // fin del método actionPerformed } // fin de la clase ManejadorBoton } // fin de la clase CargarAudioYReproducir

Figura 21.5 | Carga y reproducción de un objeto AudioClip. (Parte 3 de 3).

Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 21.5 Al reproducir clips de audio en un applet o aplicación, debe proporcionar un mecanismo para que el usuario pueda deshabilitar el audio.

21.6 Reproducción de video y otros medios con el Marco de trabajo de medios de Java Un video simple puede transmitir de manera concisa y eficiente una gran cantidad de información. Al reconocer el valor de incluir herramientas de multimedia extensibles en Java, Sun Microsystems, Intel y Silicon Graphics trabajaron en conjunto para producir la API del Marco de trabajo de medios de Java (JMF), que vimos brevemente en la sección 21.1. Mediante el uso de la API JMF, los programadores pueden crear aplicaciones en Java para reproducir, editar, transmitir y capturar muchos tipos de medios populares. Mientras que las características de la API JMF son bastante extensas, en esta sección introduciremos brevemente algunos formatos de medios populares, y demostraremos cómo reproducir video mediante el uso de la API JMF. IBM y Sun desarrollaron la especificación más reciente de la API JMF: la versión 2.0. Sun también proporciona una implementación de referencia de la especificación de la API JMF (JMF 2.1.1e), la cual soporta tipos de archivos de medios como .avi (Microsoft Audio/Video Interleave), .swf (películas de Macromedia Flash 2), .spl (Future Splash), .mp3 (Audio MPEG nivel 3), .mid o .midi (MIDI; Interfaz digital de instrumentos musicales), .mpeg y .mpg (videos MPEG-1), .mov (QuickTime), .au (formato de archivo Sun Audio) y .aif o .aiff (formato de archivo Macintosh AIFF). Ya hemos visto algunos de estos tipos de archivos. En la actualidad, la API JMF está disponible como una extensión separada del Kit de desarrollo de software para Java 2. La implementación más reciente de la API JMF (2.1.1e) se puede descargar de: java.sun.com/products/java-media/jmf/2.1.1/download.html

Necesita aceptar el contrato de licencia antes de descargar el archivo. El sitio Web JMF proporciona versiones de la API JMF que aprovechan las características de rendimiento de ciertas plataformas. Por ejemplo, el JMF Windows Performance Pack proporciona un extenso soporte para medios y dispositivos, para los programas de Java que se ejecutan en plataformas Microsoft Windows. El sitio Web oficial de la API JMF (java.sun.com/products/java-media/jmf) proporciona un soporte que se actualiza en forma continua, información y recursos para los programadores de la API JMF. Una vez que el archivo termine de descargarse, ábralo y siga las instrucciones en pantalla para instalar el programa. Deje todas las opciones predeterminadas. Tal vez necesite reiniciar su equipo para terminar la instalación.

www.elsolucionario.net

21.6 Reproducción de video y otros medios con el Marco de trabajo de medios de Java

873

Creación de un reproductor de medios simple La API JMF ofrece varios mecanismos para reproducir medios. El mecanismo más simple es usar objetos que implementen a la interfaz Player, declarada en el paquete javax.media. El paquete javax.media y sus subpaquetes contienen las clases que componen el Marco de trabajo de medios de Java. Para reproducir un clip de medios, primero debe crear un objeto URL que haga referencia a él. Después debe pasar el URL como argumento para el método static createRealizedPlayer de la clase Manager, para obtener un objeto Player para el clip de medios. La clase Manager declara métodos utilitarios para acceder a los recursos del sistema para reproducir y manipular medios. En la figura 21.6 se declara un objeto JPanel que demuestra algunos de estos métodos.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

// Fig 21.6: PanelMedios.java // Un objeto JPanel que reproduce medios de un URL import java.awt.BorderLayout; import java.awt.Component; import java.io.IOException; import java.net.URL; import javax.media.CannotRealizeException; import javax.media.Manager; import javax.media.NoPlayerException; import javax.media.Player; import javax.swing.JPanel; public class PanelMedios extends JPanel { public PanelMedios( URL urlMedios ) { setLayout( new BorderLayout() ); // usa un objeto BorderLayout // Usa componentes ligeros para compatibilidad con Swing Manager.setHint( Manager.LIGHTWEIGHT_RENDERER, true ); try { // crea un objeto Player para reproducir los medios especificados en el URL Player reproductorMedios = Manager.createRealizedPlayer( urlMedios ); // obtiene los componentes para el video y los controles de reproducción Component video = reproductorMedios.getVisualComponent(); Component controles = reproductorMedios.getControlPanelComponent(); if ( video != null ) add( video, BorderLayout.CENTER ); // agrega el componente de video if ( controles != null ) add( controles, BorderLayout.SOUTH ); // agrega los controles reproductorMedios.start(); // empieza a reproducir el clip de medios } // fin de try catch ( NoPlayerException noPlayerException ) { System.err.println( "No se encontro reproductor de medios" ); } // fin de catch catch ( CannotRealizeException cannotRealizeException ) { System.err.println( "No se pudo realizar el reproductor de medios" ); } // fin de catch catch ( IOException iOException ) {

Figura 21.6 | Objeto JPanel que reproduce un archivo de medios de un URL. (Parte 1 de 2).

www.elsolucionario.net

874

49 50 51 52

Capítulo 21

Multimedia: applets y aplicaciones

System.err.println( "Error al leer del origen" ); } // fin de catch } // fin del constructor de PanelMedios } // fin de la clase PanelMedios

Figura 21.6 | Objeto JPanel que reproduce un archivo de medios de un URL. (Parte 2 de 2). El constructor (líneas 15 a 51) establece el objeto JPanel para reproducir el archivo de medios especificado por el parámetro URL. PanelMedios utiliza un BorderLayout (línea 17). En la línea 20 se invoca al método static setHint para establecer la bandera Manager.LIGHTWEIGHT_RENDERER en true. Esto indica al objeto Manager que debe usar un renderizador ligero que sea compatible con los componentes ligeros de Swing, en contraste al renderizador pesado predeterminado. Dentro del bloque try (líneas 22 a 38), en la línea 25 se invoca el método static createRealizedPlayer de la clase Manager, para crear y realizar un objeto Player que reproduzca el archivo de medios. Cuando se realiza un objeto Player, identifica los recursos del sistema que necesita para reproducir los medios. Dependiendo del archivo, la realización puede ser un proceso que consume muchos recursos y tiempo. El método createRealizedPlayer lanza tres excepciones verificadas, NoPlayerException, CannotRealizeException e IOException. Una excepción NoPlayerException indica que el sistema no pudo encontrar un reproductor para el formato del archivo. Una excepción CannotRealizeException indica que el sistema no pudo identificar apropiadamente los recursos que necesita un archivo de medios. Una excepción IOException indica que hubo un error al leer el archivo. Estas excepciones se manejan en el bloque catch de las líneas 39 a 50. En la línea 28 se invoca el método getVisualComponent de Player para obtener un objeto Component que muestre el aspecto visual (por lo general, video) del archivo de medios. En la línea 29 se invoca el método getControlPanelComponent de Player para obtener un objeto Component que proporcione controles de reproducción y medios. Estos componentes se asignan a las variables locales video y controles, respectivamente. La instrucción if en las líneas 31 y 32, y en las líneas 34 y 35 agregan los objetos video y controles, si existen. El objeto Component llamado video se agrega a la región CENTER (línea 32), para llenar cualquier espacio disponible en el objeto JPanel. El objeto Component llamado controles, que se agrega a la región SOUTH, por lo general, proporciona los siguientes controles: 1. Un control deslizable de posicionamiento, para saltar a ciertos puntos en el clip de medios. 2. Un botón de pausa. 3. Un botón de volumen, que proporciona el control del volumen al hacer clic con el botón derecho del ratón, y una función de silencio (muting) al hacer clic con el botón izquierdo. 4. Un botón de propiedades de medios, que proporciona información detallada sobre los medios al hacer clic con el botón izquierdo del ratón, y un control de la velocidad de trama al hacer clic con el botón derecho. En la línea 37 se llama al método start de Player para empezar a reproducir el archivo de medios. En las líneas 39 a 50 se manejan las diversas excepciones que lanza createRealizedPlayer. La aplicación en la figura 21.7 muestra un cuadro de diálogo JFileChooser para que el usuario seleccione un archivo de medios. Después crea un objeto PanelMedios que reproduce el archivo seleccionado y crea un objeto JFrame para mostrar el objeto PanelMedios.

1 2 3 4 5

// Fig. 21.7: PruebaMedios.java // Un reproductor de medios simple import java.io.File; import java.net.MalformedURLException; import java.net.URL;

Figura 21.7 | Aplicación de prueba que crea un objeto PanelMedios a partir de un archivo seleccionado por el usuario. (Parte 1 de 3).

www.elsolucionario.net

21.6 Reproducción de video y otros medios con el Marco de trabajo de medios de Java

6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

875

import javax.swing.JFileChooser; import javax.swing.JFrame; public class PruebaMedios { // inicia la aplicación public static void main( String args[] ) { // crea un selector de archivo JFileChooser selectorArchivo = new JFileChooser(); // muestra cuadro de diálogo para abrir archivo int resultado = selectorArchivo.showOpenDialog( null ); if ( resultado == JFileChooser.APPROVE_OPTION ) // el usuario eligió un archivo { URL urlMedios = null; try { // obtiene el archivo como un URL urlMedios = selectorArchivo.getSelectedFile().toURL(); } // fin de try catch ( MalformedURLException malformedURLException ) { System.err.println( "No se pudo crear URL para el archivo" ); } // fin de catch if ( urlMedios != null ) // sólo lo muestra si hay un URL válido { JFrame pruebaMedios = new JFrame( "Probador de medios" ); pruebaMedios.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); PanelMedios panelMedios = new PanelMedios( urlMedios ); pruebaMedios.add( panelMedios );

} } // } // fin } // fin de

pruebaMedios.setSize( 300, 300 ); pruebaMedios.setVisible( true ); // fin de if interior fin de if exterior de main la clase PruebaMedios

Figura 21.7 | Aplicación de prueba que crea un objeto PanelMedios a partir de un archivo seleccionado por el usuario. (Parte 2 de 3).

www.elsolucionario.net

876

Capítulo 21

Multimedia: applets y aplicaciones

Figura 21.7 | Aplicación de prueba que crea un objeto PanelMedios a partir de un archivo seleccionado por el usuario. (Parte 3 de 3). El método main (líneas 12 a 46) asigna un nuevo objeto JFileChooser a la variable local selectorArchivo (línea 15), muestra un cuadro de diálogo para abrir un archivo (línea 18) y asigna el valor de retorno a resultado. En la línea 20 se comprueba resultado para determinar si el usuario eligió un archivo. Para crear un objeto Player y reproducir el archivo de medios seleccionado, debe convertir el objeto File devuelto por JFileChooser en un objeto URL. El método toURL de la clase File devuelve un URL que apunta al objeto File en el sistema, con la posibilidad de lanzar una excepción MalformedURLException si no puede crear un objeto URL para el objeto File. La instrucción try (líneas 24 a 32) crea un objeto URL para el archivo seleccionado y lo asigna a urlMedios. La instrucción if en las líneas 34 a 44 comprueba que urlMedios no sea null y crea los componentes de GUI para reproducir los medios.

21.7 Conclusión En este capítulo aprendió a crear aplicaciones más excitantes, al incluir sonido, imágenes, gráficos y video. Presentamos las herramientas de multimedia de Java, incluyendo la API del Marco de trabajo de medios de Java, la API de Sonido de Java y la API Java 3D. Utilizó las clases Image e ImageIcon para mostrar y manipular imágenes almacenadas en archivos, y aprendió acerca de los distintos formatos de imágenes en un orden específico. Utilizó mapas de imágenes para hacer que una aplicación tenga más interactividad. Después aprendió a cargar clips de audio, y a reproducirlos una vez o en un ciclo continuo. El capítulo concluyó con una demostración de cómo cargar y reproducir video. En el siguiente capítulo, continuará su estudio sobre los conceptos de GUI, partiendo de las técnicas que aprendió en el capítulo 11.

21.8 Recursos Web www.nasa.gov/multimedia/highlights/index.html

La galería NASA Multimedia Gallery (Galería de multimedios de NASA) contiene una amplia variedad de imágenes, clips de audio y de video que puede descargar y utilizar para probar sus programas de multimedia de Java. www.anbg.gov.au/anbg/index.html

El sitio Web Australian National Botanic Gardens (Jardines botánicos nacionales australianos) proporciona vínculos a los sonidos de muchos animales. Por ejemplo, pruebe el vínculo Common Birds (Aves comunes) bajo la sección “Animals in the Gardens” (Animales en los jardines). www.thefreesite.com

Este sitio tiene vínculos a sonidos e imágenes prediseñadas gratuitas. www.soundcentral.com

SoundCentral proporciona clips de audio en los formatos WAV, AU, AIFF y MIDI. www.animationfactory.com

El sitio Animation Factory (Fábrica de animaciones) proporciona miles de animaciones GIF gratuitas para uso personal.

www.elsolucionario.net

Resumen

877

www.clipart.com

ClipArt.com es un servicio basado en suscripciones, que ofrece imágenes y sonidos. www.pngart.com

PNGART.com proporciona cerca de 50,000 imágenes gratuitas en formato PNG. java.sun.com/developer/techDocs/hi/repository

El sitio Java look and feel Graphics Repository (Depósito de gráficos de apariencia visual de Java) proporciona imágenes diseñadas para utilizarse en una GUI de Swing, incluyendo imágenes de botones de barras de herramientas. www.freebyte.com/graphicprograms/

Esta guía contiene vínculos a varios programas de software de gráficos gratuitos. El software se puede utilizar para modificar imágenes y dibujar gráficos. graphicssoft.about.com/od/pixelbasedfreewin/

Este sitio Web proporciona vínculos para programas de gráficos gratuitos, diseñados para usarse en equipos Windows.

Referencias de la API de multimedia de Java java.sun.com/products/java-media/jmf/

Ésta es la página de inicio del Marco de trabajo de medios de Java (JMF). Aquí puede descargar la implementación de Sun más reciente de la JMF. Este sitio también contiene la documentación para la JMF. java.sun.com/products/java-media/sound/

La página de inicio de la API de Sonido de Java. Esta API proporciona herramientas para reproducir y grabar audio. java3d.dev.java.net/

La página de inicio de la API Java 3D. Esta API se puede utilizar para producir imágenes tridimensionales, típicas de los videojuegos actuales. java.sun.com/developer/onlineTraining/java3d/

Este sitio proporciona un tutorial sobre la API Java 3D. java.sun.com/products/java-media/jai/

La página de inicio de la API de Manipulación avanzada de imágenes de Java. Esta API proporciona herramientas de procesamiento de imágenes, como la mejora del contraste, recorte, escalado y deformación (warping) geométrica. java.sun.com/products/java-media/speech/

La API de Reconocimiento de voz de Java permite a los programas realizar la síntesis y el reconocimiento de voz. freetts.sourceforge.net/docs/index.php

FreeTTS es una implementación de la API de Reconocimiento de voz de Java. java.sun.com/products/java-media/2D/

Ésta es la página de inicio de la API Java 2D. Esta API (presentada en el capítulo 12) proporciona herramientas para gráficos bidimensionales complejos. java.sun.com/javase/6/docs/technotes/guides/imageio/index.html

Este sitio contiene una guía para la API de E/S de imágenes de Java, la cual permite a los programas cargar y guardar imágenes, usando formatos que las APIs de Java no soportan actualmente.

Resumen Sección 21.2 Cómo cargar, mostrar y escalar imágenes • El método getImage de Applet carga un objeto Image. • El método getDocumentBase de Applet devuelve la ubicación del archivo HTML del applet en Internet, como un objeto de la clase URL. • Java soporta varios formatos de imagen, incluyendo GIF (Formato de intercambio de gráficos), JPEG (Grupo unido de expertos en fotografía) y PNG (Gráficos portables de red). Los nombres de archivo para estos tipos terminan con .gif, .jpg (o .jpeg) y .png, respectivamente. • La clase ImageIcon proporciona constructores que permiten inicializar un objeto ImageIcon con una imagen del equipo local, o almacenadas en un servidor Web en Internet. • El método drawImage de Graphics acepta cuatro argumentos: una referencia al objeto Image en el que se almacena la imagen, las coordenadas x y y en donde debe mostrarse la imagen y una referencia a un objeto ImageObserver.

www.elsolucionario.net

878

Capítulo 21

Multimedia: applets y aplicaciones

• Otra versión del método drawImage de Graphics imprime en pantalla una imagen escalada. Los argumentos cuarto y quinto especifican la anchura y la altura de la imagen, para fines de mostrarla en pantalla. • La interfaz ImageObserver se implementa mediante la clase Component. Los objetos ImageObserver reciben notificaciones a medida que se carga un objeto Image y se actualiza en pantalla, en caso de que no haya estado completo al momento de mostrarlo. • El método paintIcon de ImageIcon muestra la imagen del objeto ImageIcon. El método requiere cuatro argumentos: una referencia al objeto Component en el que se mostrará la imagen, una referencia al objeto Graphics utilizado para desplegar (render) la imagen, la coordenada x de la esquina superior izquierda de la imagen y la coordenada y de la esquina superior izquierda. • Un objeto URL representa a un Localizador uniforme de recursos, el cual es un apuntador a un recurso en World Wide Web, en su equipo o en cualquier equipo conectado en red.

Sección 21.3 Animación de una serie de imágenes • Los objetos Timer generan eventos ActionEvent en intervalos de milisegundos fijos. El constructor de Timer recibe un retraso en milisegundos y un objeto ActionListener. El método start de Timer inicia el objeto Timer. El método stop indica que el objeto Timer debe dejar de generar eventos. El método restart indica que el objeto Timer debe empezar a generar eventos de nuevo.

Sección 21.4 Mapas de imágenes • Un mapa de imágenes es una imagen que tiene áreas activas en las que el usuario puede hacer clic para realizar una tarea, como cargar una página Web distinta en un explorador Web.

Sección 21.5 Carga y reproducción de clips de audio • El método play de Applet tiene dos formas: public void play ( URL ubicacion, String nombreArchivoSonido ); public void play ( URL urlSonido );

Una versión carga desde ubicacion el clip de audio almacenado en el archivo nombreArchivoSonido y lo reproduce. La otra versión recibe un URL que contiene la ubicación y el nombre de archivo del clip de audio. • El método getDocumentBase de Applet indica la ubicación del archivo HTML que cargó el applet. El método getCodeBase indica en dónde se encuentra el archivo .class para un applet. • El motor de sonido que reproduce clips de audio soporta varios formatos de archivo de audio, incluyendo .au (formato de archivo Sun Audio), .wav (formato de archivo Windows Wave), .aif o .aiff (formato de archivo Macintosh AIFF) y .mid o .rmi (formato de archivo de Interfaz digital de instrumentos musicales, MIDI). El Marco de trabajo de medios de Java (JMF) soporta formatos adicionales. • El método getAudioClip de Applet tiene dos formas que reciben los mismos argumentos que el método play. El método getAudioClip devuelve una referencia a un objeto AudioClip. Los objetos AudioClip tienen tres métodos: play, loop y stop. El método play reproduce el clip de audio sólo una vez. El método loop reproduce en forma continua el clip de audio. El método stop termina un clip de audio que se esté reproduciendo en un momento dado.

Sección 21.6 Reproducción de video y otros medios con el Marco de trabajo de medios de Java • Sun Microsystems, Intel y Silicon Graphics trabajaron en conjunto para producir el Marco de trabajo de medios de Java (JMF). • El paquete javax.media y sus subpaquetes contienen las clases que componen el Marco de trabajo de medios de Java. • La clase Manager declara métodos utilitarios para acceder a los recursos del sistema para reproducir y manipular medios. • El método toURL de la clase File devuelve un URL que apunta al objeto File en el sistema.

Terminología .aif, extensión de archivo .aiff, extensión de archivo .au, extensión de archivo .avi, extensión de archivo .gif, extensión de archivo

.jpeg, extensión de archivo .jpg, extensión de archivo .mid, extensión de archivo .mov, extensión de archivo .mp3, extensión de archivo

www.elsolucionario.net

Ejercicios de autoevaluación .mpeg, extensión de archivo .mpg, extensión de archivo .png, extensión de archivo .rmi, extensión de archivo .spl, extensión de archivo .swf, extensión de archivo .wav, extensión de archivo

javax.media, paquete LIGHTWEIGHT_RENDERER, constante de la loop, método de la interfaz AudioClip

área activa AudioClip, interfaz CannotRealizePlayerException,

excepción

clip de audio createRealizedPlayer, método de la clase Manager Dimension, clase drawImage, método de la clase Graphics

E/S de imágenes de Java, API Formato de intercambio de gráficos (GIF) Future Splash (.spl), archivos getAudioClip, método de la clase Applet getCodeBase, método de la clase Applet getControlPanelComponent, método de la interfaz Player getDocumentBase, método de la clase Applet getImage, método de la clase Applet getMinimumSize, método de la clase Component getPreferredSize, método de la clase Component getVisualComponent, método de la interfaz Player

Gráficos portables de red (PNG) Graphics, clase Grupo unido de expertos en fotografía (JPEG) Image, clase ImageIcon, clase ImageObserver, interfaz Interfaz digital de instrumentos musicales (MIDI), formato de archivo (extensiones .mid o .rmi) Java 3D, API

879

clase Manager

Macintosh AIFF, formato de archivo (extensiones .aif o .aiff) Macromedia Flash 2, películas (.swf) Manager, clase Manipulación avanzada de imágenes de Java, API mapa de imágenes Marco de trabajo de medios de Java (JMF), API Microsoft Audio/Video Interleave (.avi), archivo motor de sonido MPEG Nivel 3, archivos de audio (.mp3) MPEG-1, videos (.mpeg, .mpg) multimedia NoPlayerException, excepción paintIcon, método de la clase ImageIcon play, método de la clase Applet play, método de la interfaz AudioClip Player, interfaz QuickTime (.mov), archivos setHint, método de la clase Manager showStatus, método de la clase Applet Síntesis de voz de Java, API sonido Sonido de Java, API start, método de la interfaz Player stop, método de la clase Timer stop, método de la interfaz AudioClip Sun Audio, formato de archivo (extensión .au) toURL, método de la clase File URL, clase video Windows Wave, formato de archivo (extensión .wav)

Ejercicios de autoevaluación 21.1

Complete las siguientes oraciones: a) El método _________________ de Applet carga una imagen en un applet. b) El método _________________ de Graphics muestra una imagen en un applet. c) Java proporciona dos mecanismos para reproducir sonidos en un applet: el método play de Applet y el método play de la interfaz _________________. d) Un _________________ es una imagen que tiene áreas activas, en las que el usuario puede hacer clic para realizar una tarea, como cargar una página Web distinta. e) El método _________________ de la clase ImageIcon muestra la imagen del objeto ImageIcon. f ) Java soporta varios formatos de imagen, incluyendo _________________, _________________ y _________________.

21.2

Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué. a) Un sonido será recolectado como basura tan pronto como haya dejado de reproducirse. b) La clase ImageIcon proporciona constructores que permiten a un objeto ImageIcon ser inicializado con sólo una imagen proveniente del equipo local. c) El método play de la clase AudioClip reproduce en forma continua un clip de audio. d) La API de E/S de imágenes de Java se utiliza para agregar gráficos en 3D a una aplicación de Java.

www.elsolucionario.net

880

Capítulo 21

Multimedia: applets y aplicaciones

e) El método getDocumentBase de Applet devuelve, como un objeto de la clase URL, la ubicación en Internet del archivo HTML que invocó al applet.

Respuestas a los ejercicios de autoevaluación 21.1 a) getImage. b) drawImage. c) AudioClip. d) mapa de imágenes. e) paintIcon. f ) Formato de intercambio de gráficos (GIF), Grupo unido de expertos en fotografía (JPEG), Gráficos portables de red (PNG). 21.2 a) Verdadero. b) Falso. ImageIcon puede cargar imágenes de Internet también. c) Falso. El método play de la clase AudioClip reproduce un clip de audio sólo una vez. El método loop de la clase AudioClip reproduce en forma continua un clip de audio. d) Falso. La API Java 3D se utiliza para crear y modificar gráficos en 3D. La API de E/S de imágenes de Java se utiliza para leer y escribir imágenes en archivos. e) Verdadero.

Ejercicios 21.3

Describa cómo hacer que una animación sea “amigable para el explorador Web”.

21.4

Describa los métodos de Java para reproducir y manipular clips de audio.

21.5

Explique cómo se utilizan los mapas de imágenes. Enliste varios ejemplos de su uso.

21.6 (Borrar una imagen al azar) Suponga que se muestra una imagen en un área rectangular de la pantalla. Una manera de borrar la imagen es simplemente asignar a cada píxel el mismo color inmediatamente, pero éste es un efecto visual torpe. Escriba un programa en Java para mostrar una imagen y luego borrarla, utilizando la generación de números aleatorios para seleccionar píxeles individuales y borrarlos. Una vez que se haya borrado la mayor parte de la imagen, borre el resto de los píxeles al mismo tiempo. Puede dibujar píxeles individuales como una línea que inicie y termine en las mismas coordenadas. Tal vez sea conveniente que pruebe distintas variantes de este problema. Por ejemplo, podría mostrar líneas o figuras al azar, para borrar regiones de la pantalla. 21.7 (Texto intermitente) Cree un programa en Java para mostrar repetidamente texto intermitente en la pantalla. Para ello, alterne el texto con una imagen de fondo simple a color. Permita al usuario controlar la “velocidad de parpadeo” y el color o patrón de fondo. Necesitará usar los métodos getDelay y setDelay de la clase Timer. Estos métodos se utilizan para recuperar y establecer el intervalo en milisegundos entre ActionEvents, respectivamente. 21.8 (Imagen intermitente) Cree un programa en Java para mostrar repetidamente una imagen intermitente en la pantalla. Para ello, alterne la imagen con una imagen de fondo simple a color. 21.9

(Reloj digital) Implemente un programa para mostrar un reloj digital en la pantalla.

21.10 (Atraer la atención hacia una imagen) Si desea enfatizar una imagen, puede colocar una fila de bombillas simuladas alrededor de su imagen. Puede dejar que las bombillas parpadeen al unísono, o puede dejar que se prendan y apaguen en secuencia, una después de otra. 21.11 (Acercamiento/alejamiento de imagen) Cree un programa que le permita acercarse o alejarse de una imagen.

Sección especial: proyectos de multimedia retadores Los ejercicios anteriores están adaptados al texto y diseñados para evaluar la comprensión del lector en cuanto a los conceptos fundamentales de multimedia. En esta sección incluimos una colección de proyectos avanzados de multimedia. El lector encontrará que estos problemas son retadores, pero interesantes. Los problemas varían considerablemente en cuanto al grado de dificultad. Algunos requieren de una o dos horas para escribir e implementar el programa correspondiente. Otros son útiles como tareas de laboratorio, que podrían requerir de dos o tres semanas de estudio e implementación. Algunos son proyectos finales retadores. (Nota para los instructores: No se proporcionan las soluciones para estos ejercicios). 21.12 (Animación) Cree un programa de animación en Java, de propósito general. Su programa deberá permitir al usuario especificar la secuencia de cuadros a mostrar, la velocidad a la que se van a mostrar las imágenes, los clips de audio que deberán reproducirse mientras la animación se ejecuta, y así sucesivamente. 21.13 (Quintillas) Modifique el programa para escribir quintillas del ejercicio 10.10, para que cante las quintillas que su programa cree. 21.14 (Transición aleatoria entre imágenes) Este proyecto proporciona un agradable efecto visual. Si va a mostrar una imagen en cierta área de la pantalla y desea cambiar a otra imagen en la misma área de la pantalla, almacene la nueva

www.elsolucionario.net

Sección especial: proyectos de multimedia retadores

881

imagen de pantalla en un búfer fuera de la pantalla, y copie al azar píxeles de la nueva imagen hacia el área que se va a mostrar en pantalla, cubriendo los píxeles anteriores en esas posiciones. Cuando se haya copiado la mayor parte de los píxeles, copie toda la nueva imagen en el área que se va a mostrar en pantalla, para asegurarse de estar mostrando la nueva imagen completa. Para implementar este programa, tal vez necesite usar las clases PixelGrabber y MemoryImageSource (consulte la documentación de las APIs de Java para obtener las descripciones de esas clases). Tal vez sea conveniente que pruebe distintas variantes de este problema. Por ejemplo, trate de seleccionar todos los píxeles en una línea recta o figura seleccionada al azar en la nueva imagen, y cubra esos píxeles por encima de las posiciones correspondientes de la imagen anterior. 21.15 (Audio de fondo) Agregue audio de fondo a una de sus aplicaciones favoritas, utilizando el método loop de la clase AudioClip para reproducir el sonido en segundo plano mientras interactúa con su aplicación en forma normal. 21.16 (Marquesina desplazable) Cree un programa en Java para desplazar caracteres punteados de derecha a izquierda (o de izquierda a derecha, si es apropiado para su lenguaje), a lo largo de un letrero tipo marquesina. Como una opción, muestre el texto en un ciclo continuo de manera que el texto desaparezca en un extremo y reaparezca en el otro. 21.17 (Marquesina con imagen desplazable) Cree un programa en Java para mostrar una imagen a lo largo de una pantalla tipo marquesina. 21.18 (Reloj análogo) Cree un programa en Java para mostrar un reloj análogo con manecillas para las horas, minutos y segundos que se desplacen apropiadamente, a medida que vaya cambiando la hora. 21.19 (Audio dinámico y calidoscopio gráfico) Escriba un programa de calidoscopio que muestre gráficos reflejados para simular el popular juguete para niños. Incorpore efectos de audio que “reflejen” los gráficos de su programa que cambian en forma dinámica. 21.20 (Generador automático de rompecabezas) Cree un generador y manipulador de rompecabezas en Java. El usuario especifica una imagen. Su programa carga y muestra la imagen, y después divide la imagen en varias figuras seleccionadas al azar, y las revuelve. El usuario entonces utiliza el ratón para desplazar las piezas alrededor y tratar de resolver el rompecabezas. Agregue sonidos de audio apropiados a medida que se vayan moviendo las piezas y se inserten en el lugar correcto. Tal vez sea conveniente tener etiquetas en cada pieza y en el lugar en el que pertenece; después se pueden utilizar efectos de audio para ayudar al usuario a obtener las piezas en las posiciones correctas. 21.21 (Programa para generar y recorrer laberintos) Desarrolle un programa basado en multimedia para generar laberintos y recorrerlos, con base en los programas de laberintos que escribió en los ejercicios 15.20 a 15.22. Deje que el usuario personalice el laberinto, especificando el número de filas y columnas junto con el nivel de dificultad. Haga que un ratón animado recorra el laberinto. Utilice audio para dramatizar el movimiento de su personaje ratón. 21.22 (Bandido de un brazo) Desarrolle una simulación multimedia de una máquina tragamonedas, conocida como “bandido de un brazo”. Debe tener tres ruedas giratorias. Coloque varias frutas y símbolos en cada rueda. Use la generación de números aleatorios para simular los giros de cada rueda y la acción de detenerse en un símbolo. 21.23 (Carrera de caballos) Cree una simulación en Java de una carrera de caballos. Debe tener varios competidores. Use clips de audio para tener un anunciador. Reproduzca los clips de audio apropiados para indicar el estado correcto de cada uno de los competidores, a lo largo de la carrera. Use clips de audio para anunciar el resultado final. Sería conveniente que tratara de simular el tipo de juegos de carreras de caballos que se juegan en los carnavales. Los jugadores toman turnos en el ratón y tienen que realizar ciertas manipulaciones hábiles con el ratón para que sus caballos avancen. 21.24 (Juego de tejo) Desarrolle una simulación basada en multimedia del juego de tejo. Use efectos de audio y visuales apropiados. 21.25 (Juego de billar) Cree una simulación basada en multimedia del juego de billar. Cada jugador toma turnos utilizando el ratón para posicionar un taco y pegarle a la bola en el ángulo apropiado para tratar de hacer que las demás bolas caigan en las buchacas. Su programa deberá llevar la puntuación. 21.26 (Artista) Diseñe un programa artístico en Java que proporcione a un artista una gran variedad de herramientas para dibujar, utilizar imágenes, animaciones, etcétera, para crear una muestra de arte dinámica con multimedia. 21.27 (Diseñador de fuegos artificiales) Cree un programa en Java que podría ser utilizado para crear una muestra de fuegos artificiales. Cree una variedad de demostraciones de fuegos artificiales. Después controle la activación de los fuegos artificiales para obtener un efecto máximo. 21.28 (Diseñador de pisos) Desarrolle un programa en Java que ayude a alguien a distribuir los muebles en su hogar. Agregue características que permitan a la persona lograr el mejor arreglo posible.

www.elsolucionario.net

882

Capítulo 21

Multimedia: applets y aplicaciones

21.29 (Crucigrama) Los crucigramas son uno de los pasatiempos más populares. Desarrolle un programa de crucigramas basado en multimedia. Su programa debe permitir al jugador colocar y borrar palabras fácilmente. Enlace su programa con un diccionario computarizado extenso. Su programa deberá también tener la capacidad de sugerir palabras en las que se hayan llenado algunas letras. Proporcione otras características que faciliten el trabajo de los entusiastas de crucigramas. 21.30 (Acertijo del 15) Escriba un programa en Java basado en multimedia que permita al usuario jugar al 15. Este juego se lleva a cabo en un tablero de 4 por 4, para un total de 16 posiciones. Una de las posiciones está vacía. Las demás posiciones están ocupadas por 15 mosaicos, numerados del 1 al 15. Cualquier mosaico enseguida de la posición actual vacía puede moverse a esa posición, haciendo clic en el mosaico. Su programa deberá crear el tablero con los mosaicos desordenados. El objetivo es ordenar los mosaicos en forma secuencial, fila por fila. 21.31 (Tiempo de reacción/Probador de precisión de reacción) Cree un programa en Java que mueva una figura creada al azar alrededor de la pantalla. El usuario desplazará el ratón para atrapar y hacer clic en la figura. Puede variarse la velocidad y el tamaño de la figura. Su programa deberá llevar las estadísticas acerca de cuánto tiempo le lleva al usuario atrapar una figura de un tamaño dado. Probablemente al usuario le sea más difícil atrapar figuras que sean más pequeñas y se muevan más rápido. 21.32 (Calendario/archivo de recordatorios) Utilizando audio e imágenes, cree un calendario de propósito general y un archivo de “recordatorios”. Por ejemplo, el programa deberá cantar “Feliz cumpleaños” cuando lo utilice en su cumpleaños. Haga que el programa muestre imágenes y reproduzca clips de audio asociados con eventos importantes. Además, haga que el programa le recuerde de antemano estos eventos importantes. Por ejemplo, sería bueno hacer que el programa le avisara con una semana de anticipación, para que pudiera recoger una tarjeta de felicitación apropiada para esa persona especial. 21.33 (Imágenes giratorias) Cree un programa en Java que le permita girar una imagen un cierto número de grados (hasta un máximo de 360 grados). El programa deberá permitirle a usted especificar que desea girar la imagen en forma continua. El programa deberá permitirle ajustar la velocidad de giro en forma dinámica. 21.34 (Colorear imágenes y fotografías en blanco y negro) Cree un programa en Java que le permita pintar de colores una fotografía en blanco y negro. Proporcione una paleta para seleccionar colores. Su programa deberá permitirle aplicar distintos colores en distintas regiones de la imagen. 21.35 (Simulador Simpletron basado en multimedia) Modifique el simulador Simpletron que desarrolló en los ejercicios de capítulos anteriores (ejercicios 7.34 a 7.36 y ejercicios 17.26 a 17.30), para incluir características de multimedia. Agregue sonidos tipo computadora para indicar que el Simpletron está ejecutando instrucciones. Agregue un sonido de vidrio quebrándose cuando ocurra un error fatal. Use luces intermitentes para indicar cuáles celdas de memoria o cuáles registros se están manipulando en ese momento. Use otras técnicas de multimedia según sea apropiado, para hacer que su simulador Simpletron sea más valioso para sus usuarios como una herramienta educativa.

www.elsolucionario.net

22 x Si un actor entra por la puerta, no tienes nada. Pero si entra por la ventana, tienes un problema.

Componentes de la GUI: parte 2

—Billy Wilder

…la fuerza de los eventos despierta talentos adormilados. —Edward Hoagland

Usted y yo veríamos la fotografía con más interés, si dejaran de preocuparse y aplicaran el sentido común al problema de registrar la apariencia visual de su propia era.

OBJETIVOS En este capítulo aprenderá a: Q

Crear y manipular controles deslizables, menús, menús contextuales y ventanas.

Q

Cambiar la apariencia visual de una GUI, utilizando la apariencia visual adaptable (PLAF) de Swing.

Q

Crear una interfaz de múltiples documentos con y JInternalFrame.

JDesktopPane Q

Utilizar los administradores de esquemas adicionales.

—Jessie Tarbox Beals

www.elsolucionario.net

Pla n g e ne r a l

884

Capítulo 22

22.1 22.2 22.3 22.4 22.5 22.6 22.7 22.8 22.9 22.10

Componentes de la GUI: parte 2

Introducción JSlider

Ventanas: observaciones adicionales Uso de menús con marcos JPopupMenu

Apariencia visual adaptable JDesktopPane y JInternalFrame JTabbedPane

Administradores de esquemas: BoxLayout y GridBagLayout Conclusión

Resumen | Terminología | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

22.1 Introducción En este capítulo continuaremos nuestro estudio acerca de las GUIs. Hablaremos sobre componentes y administradores de esquemas adicionales, y estableceremos las bases para crear las GUIs más complejas. Empezaremos nuestra discusión con los menús, que permiten al usuario realizar con efectividad tareas en el programa. La apariencia visual de una GUI de Swing puede ser uniforme a través de todas las plataformas en las que se ejecuta un programa de Java, o la GUI puede personalizarse mediante el uso de la apariencia visual adaptable (PLAF) de Swing. Proporcionaremos un ejemplo que ilustra cómo cambiar entre la apariencia visual metálica de Swing (que tiene una apariencia y comportamiento similares entre las distintas plataformas), una apariencia visual que simula a Motif (una apariencia visual de UNIX muy popular) y una que simula la apariencia visual de Microsoft Windows. Muchas de las aplicaciones de hoy en día utilizan una interfaz de múltiples documentos (MDI): una ventana principal (conocida también como la ventana padre) que contiene a otras ventanas (a menudo conocidas como ventanas hijas) para administrar varios documentos abiertos en paralelo. Por ejemplo, muchos programas de correo electrónico le permiten tener varias ventanas abiertas al mismo tiempo, para que pueda componer o leer varios mensajes de correo electrónico. En este capítulo demostraremos el uso de las clases de Swing para crear interfaces con múltiples documentos. Finalmente, terminaremos el capítulo con una serie de ejemplos acerca de los administradores de esquemas adicionales que están disponibles para organizar interfaces gráficas de usuario. Swing es un tema extenso y complejo. Existen muchos más componentes de la GUI y herramientas de las que podemos presentar aquí. Presentaremos más componentes de la GUI de Swing en los capítulos restantes de este libro, a medida que se vayan necesitando. En nuestro libro Advanced Java 2 Platform How to Program hablamos sobre otros componentes y herramientas más avanzadas de Swing.

22.2 JSlider

Los objetos JSlider permiten al usuario seleccionar de entre un rango de valores enteros. La clase JSlider hereda de JComponent. En la figura 22.1 se muestra un objeto JSlider horizontal con marcas y el indicador que permite al usuario seleccionar un valor. Los objetos JSlider pueden personalizarse para mostrar marcas más distanciadas, marcas menos distanciadas y etiquetas para las marcas. También soportan el ajuste a la marca, en el que al colocar el indicador entre dos marcas, éste se ajusta a la marca más cercana. La mayoría de los componentes de la GUI de Swing soportan las interacciones del usuario mediante el ratón y el teclado. Por ejemplo, si un objeto JSlider tiene el foco (es decir, que sea el componente de la GUI seleccio-

Indicador

Marca

Figura 22.1 | Componente JSlider con orientación horizontal.

www.elsolucionario.net

22.2

JSlider

885

nado en ese momento, en la interfaz de usuario), la tecla de flecha izquierda y la tecla de flecha derecha hacen que el indicador del objeto JSlider se decremente o se incremente en 1, respectivamente. La tecla de flecha hacia abajo y la tecla de flecha hacia arriba también hacen que el indicador del objeto JSlider se decremente o incremente en 1 marca, respectivamente. Las teclas Av Pág (avance de página) y Re Pág (retroceso de página) hacen que el indicador del objeto JSlider se decremente o incremente en incrementos de bloque de una décima parte del rango de valores, respectivamente. La tecla Inicio desplaza el indicador hacia el valor mínimo del objeto JSlider, y la tecla Fin lo desplaza hacia el valor máximo del objeto JSlider. Los objetos JSlider tienen una orientación horizontal o una vertical. Para un objeto JSlider horizontal, el valor mínimo se encuentra en el extremo izquierdo y el valor máximo, en el derecho. Para un objeto JSlider vertical, el valor mínimo se encuentra en el extremo inferior y el valor máximo, en el superior. Las posiciones de los valores mínimo y máximo del objeto JSlider se pueden invertir mediante la invocación del método setInverted de JSlider con el argumento boolean true. La posición relativa del indicador muestra el valor actual del objeto JSlider. El programa de las figuras 22.2 y 22.4 permite al usuario ajustar el tamaño de un círculo dibujado en una subclase de JPanel, llamada PanelOvalo (figura 22.2). El usuario especifica el diámetro del círculo con un objeto JSlider horizontal. La clase PanelOvalo sabe cómo dibujar un círculo en sí misma, utilizando su propia variable de instancia diametro para determinar el diámetro del círculo; el diametro se utiliza como la anchura y altura del cuadro delimitador en el que se muestra el círculo. El valor del diametro se establece cuando el usuario interactúa con el objeto JSlider. El manejador de eventos llama al método establecerDiametro en la clase PanelOvalo para establecer el diametro, y llama a repaint para dibujar el nuevo círculo. La llamada a repaint resulta en una llamada al método paintComponent de PanelOvalo.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

// Fig. 22.2: PanelOvalo.java // Una clase JPanel personalizada. import java.awt.Graphics; import java.awt.Dimension; import javax.swing.JPanel; public class PanelOvalo extends JPanel { private int diametro = 10; // diámetro predeterminado de 10 // dibuja un óvalo del diámetro especificado public void paintComponent( Graphics g ) { super.paintComponent( g ); g.fillOval( 10, 10, diametro, diametro ); // dibuja un círculo } // fin del método paintComponent // valida y establece el diámetro, después vuelve a pintar public void establecerDiametro( int nuevoDiametro ) { // si el diámetro es inválido, usa el valor predeterminado de 10 diametro = ( nuevoDiametro >= 0 ? nuevoDiametro : 10 ); repaint(); // vuelve a pintar el panel } // fin del método establecerDiametro // lo utiliza el administrador de esquemas para determinar el tamaño preferido public Dimension getPreferredSize() { return new Dimension( 200, 200 ); } // fin del método getPreferredSize // lo utiliza el administrador de esquemas para determinar el tamaño mínimo

Figura 22.2 | Subclase de JPanel para dibujar círculos de un diámetro especificado. (Parte 1 de 2).

www.elsolucionario.net

886

34 35 36 37 38

Capítulo 22

Componentes de la GUI: parte 2

public Dimension getMinimumSize() { return getPreferredSize(); } // fin del método getMinimumSize } // fin de la clase PanelOvalo

Figura 22.2 | Subclase de JPanel para dibujar círculos de un diámetro especificado. (Parte 2 de 2). La clase PanelOvalo (figura 22.2) contiene un método paintComponent (líneas 12 a 17) que dibuja un óvalo relleno (un círculo en este ejemplo), un método establecerDiametro (líneas 20 a 25) que modifica el diametro del círculo y vuelve a pintar (repaint) el PanelOvalo, un método getPreferredSize (líneas 28 a 31) que devuelve la anchura y altura preferidas de un objeto PanelOvalo, y un método getMinimumSize (líneas 34 a 37) que devuelve la anchura y altura mínimas de un objeto PanelOvalo. Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 22.1 Si un nuevo componente de la GUI tiene una anchura y altura mínimas (es decir, que unas medidas menores hagan que el componente se muestre incorrectamente en la pantalla), debe sobrescribir el método getMinimumSize para devolver la anchura y altura mínimas como una instancia de la clase Dimension.

Observación de ingeniería de software 22.1 Para muchos componentes de la GUI, el método getMinimumSize se implementa para devolver el resultado de una llamada al método getPreferredSize de ese componente.

La clase MarcoSlider (figura 22.3) crea el objeto JSlider que controla el diámetro del círculo. El constructor de la clase MarcoSlider (líneas 17 a 45) crea el objeto PanelOvalo llamado miPanel (línea 21) y establece su color de fondo (línea 22). En las líneas 25 y 26 se crea el objeto JSlider llamado diametroJSlider para controlar el diámetro del círculo que se dibuja en el PanelOvalo. El constructor de JSlider recibe cuatro argumentos. El primero especifica la orientación del diametroJSlider, que es HORIZONTAL (una constante en la interfaz SwingConstants). Los argumentos segundo y tercero indican los valores enteros mínimo y máximo en el rango de valores para este objeto JSlider. El último argumento indica que el valor inicial del objeto JSlider (es decir, en dónde se muestra el indicador) debe ser 10. En las líneas 27 y 28 se personaliza la apariencia del objeto JSlider. El método setMajorTickSpacing indica que cada marca principal representa 10 valores en el rango soportado por el objeto JSlider. El método setPaintTicks con un argumento true indica que las marcas deben mostrarse (no se muestran de manera predeterminada). Para los otros métodos que se utilizan para personalizar la apariencia de un objeto JSlider, consulte la documentación en línea de la clase JSlider (java.sun.com/javase/6/docs/api/javax/swing/ JSlider.html).

1 2 3 4 5 6 7 8 9 10 11 12 13

// Fig. 22.3: MarcoSlider.java // Uso de objetos JSlider para cambiar el tamaño de un óvalo. import java.awt.BorderLayout; import java.awt.Color; import javax.swing.JFrame; import javax.swing.JSlider; import javax.swing.SwingConstants; import javax.swing.event.ChangeListener; import javax.swing.event.ChangeEvent; public class MarcoSlider extends JFrame { private JSlider diametroJSlider; // control deslizable para seleccionar el diámetro

Figura 22.3 | Valor de

JSlider

que se utiliza para determinar el diámetro de un círculo. (Parte 1 de 2).

www.elsolucionario.net

22.2

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46

JSlider

887

private PanelOvalo miPanel; // panel para dibujar el círculo // constructor sin argumentos public MarcoSlider() { super( "Demostracion de JSlider" ); miPanel = new PanelOvalo(); // crea panel para dibujar el círculo miPanel.setBackground( Color.YELLOW ); // establece el color de fondo en amarillo // establece objeto JSlider para controlar el valor del diámetro diametroJSlider = new JSlider( SwingConstants.HORIZONTAL, 0, 200, 10 ); diametroJSlider.setMajorTickSpacing( 10 ); // crea una marca cada 10 diametroJSlider.setPaintTicks( true ); // dibuja las marcas en el control deslizable // registra el componente que escucha los eventos del objeto JSlider diametroJSlider.addChangeListener( new ChangeListener() // clase interna anónima { // maneja el cambio en el valor del control deslizable public void stateChanged( ChangeEvent e ) { miPanel.establecerDiametro( diametroJSlider.getValue() ); } // fin del método stateChanged } // fin de la clase interna anónima ); // fin de la llamada a addChangeListener add( add( } // fin } // fin de

diametroJSlider, BorderLayout.SOUTH ); // agrega el control deslizable al marco miPanel, BorderLayout.CENTER ); // agrega el panel al marco del constructor de MarcoSlider la clase MarcoSlider

Figura 22.3 | Valor de

JSlider

que se utiliza para determinar el diámetro de un círculo. (Parte 2 de 2).

Los objetos JSlider generan eventos ChangeEvent (paquete javax.swing.event) en respuesta a las interacciones del usuario. Un objeto de una clase que implementa a la interfaz ChangeListener (paquete javax. swing.event) y declara el método stateChanged puede responder a los eventos ChangeEvent. En las líneas 31 a 41 se registra un objeto ChangeListener para manejar los eventos de diametroJSlider. Cuando se llama al método stateChanged (líneas 36 a 39) en respuesta a una interacción del usuario, en la línea 38 se hace una llamada al método establecerDiametro de miPanel y se pasa el valor actual del objeto JSlider como argumento. El método getValue de JSlider devuelve la posición actual del indicador.

1 2 3 4 5 6 7 8 9 10

// Fig. 22.4: DemoSlider.java // Prueba de MarcoSlider. import javax.swing.JFrame; public class DemoSlider { public static void main( String args[] ) { MarcoSlider marcoSlider = new MarcoSlider(); marcoSlider.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

Figura 22.4 | Clase de prueba para MarcoSlider. (Parte 1 de 2).

www.elsolucionario.net

888

11 12 13 14

Capítulo 22

Componentes de la GUI: parte 2

marcoSlider.setSize( 220, 270 ); // establece el tamaño del marco marcoSlider.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase DemoSlider

Figura 22.4 | Clase de prueba para MarcoSlider. (Parte 2 de 2).

22.3 Ventanas: observaciones adicionales En esta sección, hablaremos sobre algunas cuestiones importantes de JFrame. Un objeto JFrame es una ventana con una barra de título y un borde. La clase JFrame es una subclase de java.awt.Frame (que es una subclase de java.awt.Window). Como tal, JFrame es uno de los pocos componentes de la GUI de Swing que no es un componente de la GUI ligero. Cuando se muestra una ventana desde un programa de Java, ésta se proporciona mediante el kit de herramientas de ventana de la plataforma local y, por lo tanto, la ventana tendrá la misma apariencia que las otras ventanas que se muestren en esa plataforma. Cuando se ejecute una aplicación de Java en una Macintosh en donde se muestre una ventana, la barra de título y los bordes de la ventana tendrán la misma apariencia que las ventanas de las demás aplicaciones Macintosh. Cuando se ejecute una aplicación de Java en Microsoft Windows y se muestre una ventana, la barra de título y los bordes tendrán la misma apariencia que las ventanas de las demás aplicaciones Microsoft Windows. Y cuando se ejecute una aplicación de Java en una plataforma UNIX y se muestre una ventana, la barra de título y los bordes de la ventana tendrán la misma apariencia que las ventanas de las demás aplicaciones UNIX en esa plataforma. La clase JFrame soporta tres operaciones cuando el usuario cierra la ventana. De manera predeterminada, una ventana se oculta (es decir, se quita de la pantalla) cuando el usuario la cierra. Esto puede controlarse con el método setDefaultCloseOperation de JFrame. La interfaz WindowConstants (paquete javax.swing), que es implementada por la clase JFrame, declara tres constantes para ser utilizadas con este método: DISPOSE_ON_CLOSE, DO_NOTHING_ON_CLOSE y HIDE_ON_CLOSE (el valor predeterminado). Algunas plataformas sólo permiten que se muestre un número limitado de ventanas en la pantalla. Por lo tanto, una ventana es un valioso recurso que debe regresarse al sistema cuando ya no se necesite. La clase Window (una superclase indirecta de JFrame) declara el método dispose para este fin. Cuando ya no se necesita un objeto Window en una aplicación, se debe desechar explícitamente este objeto Window. Esto puede hacerse llamando al método dispose de Window, o llamando al método setDefaultCloseOperation con el argumento WindowConstants.DISPOSE_ON_CLOSE. Al terminar una aplicación se devuelven todos los recursos de ventana al sistema. Al establecer la operación de cierre predeterminada en DO_NOTHING_ON_CLOSE, el programa determinará lo que debe hacer cuando el usuario le indique que debe cerrar la ventana.

Tip de rendimiento 22.1 Una ventana es un recurso imprescindible del sistema. Devuélvala al sistema cuando ya no sea necesaria.

De manera predeterminada, una ventana no se muestra en la pantalla sino hasta que el programa invoque al método setVisible de esta ventana (heredado de la clase java.awt.Component) con un argumento true. Ade-

www.elsolucionario.net

22.4 Uso de menús con marcos

889

más, debe establecerse el tamaño de una ventana mediante una llamada al método setSize (heredado de la clase java.awt.Component). La posición que tendrá la ventana cuando aparezca en la pantalla se especifica mediante el método setLocation (heredado de la clase java.awt.Component).

Error común de programación 22.1 Olvidar llamar al método setVisible en una ventana es un error lógico en tiempo de ejecución; la ventana no se mostrará en pantalla.

Error común de programación 22.2 Olvidar llamar al método barra de título.

setSize

en una ventana es un error lógico en tiempo de ejecución; sólo aparecerá la

Las ventanas generan eventos de ventana cuando el usuario las manipula. Los componentes de escucha de eventos se registran para eventos de ventana mediante el método addWindowListener de la clase Window. La interfaz WindowListener proporciona siete métodos manejadores de eventos de ventana: windowActivated (se invoca cuando el usuario hace que la ventana sea la ventana activa), windowClosed (se invoca cuando se cierra la ventana), windowClosing (se invoca cuando el usuario inicia el cierre de la ventana), windowDeactivated (se invoca cuando el usuario hace que otra ventana sea la ventana activa), windowDeiconified (se invoca cuando el usuario restaura una ventana, después de que ha sido minimizada), windowIconified (se invoca cuando el usuario minimiza una ventana) y windowOpened (se invoca cuando un programa muestra por primera vez una ventana en la pantalla).

22.4 Uso de menús con marcos Los menús son una parte integral de las GUIs. Permiten al usuario realizar acciones sin saturar innecesariamente una GUI con componentes adicionales. En las GUIs de Swing, los menús pueden adjuntarse solamente a los objetos de clases que proporcionen el método setJMenuBar. Dos de esas clases son JFrame y JApplet. Las clases utilizadas para declarar menús son JMenuBar, JMenu, JMenuItem, JCheckBoxMenuItem y la clase JRadioButtonMenuItem. Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 22.2 Los menús simplifican las GUIs, ya que se pueden ocultar componentes dentro de ellos. Estos componentes estarán visibles sólo cuando el usuario los busque, al seleccionar el menú.

La clase JMenuBar (una subclase de JComponent) contiene los métodos necesarios para administrar una barra de menús, la cual es un contenedor para los menús. La clase JMenu (una subclase de javax.swing.JMenuItem) contiene los métodos necesarios para administrar menús. Los menús contienen elementos de menú, y pueden agregarse a barras de menús o a otros menús, como submenús. Cuando se hace clic en un menú, éste se expande para mostrar su lista de elementos de menú. La clase JMenuItem (una subclase de javax.swing.AbstractButton) contiene los métodos necesarios para administrar elementos de menú. Un elemento de menú es un componente de la GUI que está dentro de un menú y que, cuando se selecciona, produce un evento de acción. Un elemento de menú puede usarse para iniciar una acción, o puede ser un submenú que proporcione más elementos de menú que el usuario pueda seleccionar. Los submenús son útiles para agrupar en un menú los elementos de menú que estén relacionados. La clase JCheckBoxMenuItem (una subclase de javax.swing.JMenuItem) contiene los métodos necesarios para administrar los elementos de menú que pueden activarse o desactivarse. Cuando se selecciona un objeto JCheckBoxMenuItem, aparece una marca de verificación a la izquierda del elemento de menú. Cuando se selecciona de nuevo el mismo objeto JCheckBoxMenuItem, se quita la marca de verificación a la izquierda del elemento de menú. La clase JRadioButtonMenuItem (una subclase de javax.swing.JMenuItem) contiene los métodos necesarios para administrar elementos de menú que pueden activarse o desactivarse, de manera parecida a los objetos JCheckBoxMenuItem. Cuando se mantienen varios objetos JRadioButtonMenuItem como parte de un objeto ButtonGroup, sólo puede seleccionarse un elemento en el grupo a la vez. Cuando se selecciona un objeto

www.elsolucionario.net

890

Capítulo 22

Componentes de la GUI: parte 2

JRadioButtonMenuItem, aparece un círculo relleno a la izquierda del elemento de menú. Cuando se selecciona otro objeto JCheckBoxMenuItem, se quita el círculo relleno a la izquierda del elemento de menú previamente seleccionado. La aplicación de las figuras 22.5 y 22.6 demuestra el uso de varios elementos de menú y cómo especificar caracteres especiales (llamados nemónicos) que pueden proporcionar un acceso rápido a un menú o elemento de menú, desde el teclado. Los nemónicos pueden usarse con todas las subclases de javax.swing.AbstractButton. La clase MarcoMenu (figura 22.5) declara los componentes de la GUI y el manejo de eventos para los elementos de menú. La mayor parte del código en esta aplicación aparece en el constructor de la clase (líneas 34 a 151).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46

// Fig. 22.5: MarcoMenu.java // Demostración de los menús. import java.awt.Color; import java.awt.Font; import java.awt.BorderLayout; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import java.awt.event.ItemListener; import java.awt.event.ItemEvent; import javax.swing.JFrame; import javax.swing.JRadioButtonMenuItem; import javax.swing.JCheckBoxMenuItem; import javax.swing.JOptionPane; import javax.swing.JLabel; import javax.swing.SwingConstants; import javax.swing.ButtonGroup; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JMenuBar; public class MarcoMenu extends JFrame { private final Color valoresColores[] = { Color.BLACK, Color.BLUE, Color.RED, Color.GREEN }; private JRadioButtonMenuItem elementosColores[]; // elementos del menú colores private JRadioButtonMenuItem fuentes[]; // elementos del menú fuentes private JCheckBoxMenuItem elementosEstilos[]; // elementos del menú estilos private JLabel mostrarJLabel; // muestra texto de ejemplo private ButtonGroup fuentesButtonGroup; // administra elementos del menú fuentes private ButtonGroup coloresButtonGroup; // administra elementos del menú colores private int estilo; // se utiliza para crear el estilo para la fuente // el constructor sin argumentos establece la GUI public MarcoMenu() { super( "Uso de objetos JMenu" ); JMenu menuArchivo = new JMenu( "Archivo" ); // crea el menú archivo menuArchivo.setMnemonic( 'A' ); // establece el nemónico a A // crea el elemento de menú Acerca de... JMenuItem elementoAcercaDe = new JMenuItem( "Acerca de..." ); elementoAcercaDe.setMnemonic( 'c' ); // establece el nemónico a c menuArchivo.add( elementoAcercaDe ); // agrega el elemento elementoAcercaDe al menú archivo elementoAcercaDe.addActionListener(

Figura 22.5 | Objetos JMenu y nemónicos. (Parte 1 de 4).

www.elsolucionario.net

22.4 Uso de menús con marcos

47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103

891

new ActionListener() // clase interna anónima { // muestra cuadro de diálogo de mensaje cuando el usuario selecciona Acerca de... public void actionPerformed( ActionEvent evento ) { JOptionPane.showMessageDialog( MarcoMenu.this, "Este es un ejemplo\ndel uso de menus", "Acerca de", JOptionPane.PLAIN_MESSAGE ); } // fin del método actionPerformed } // fin de la clase interna anónima ); // fin de la llamada a addActionListener JMenuItem elementoSalir = new JMenuItem( "Salir" ); // crea el elemento salir elementoSalir.setMnemonic( 'S' ); // establece el nemónico a S menuArchivo.add( elementoSalir ); // agrega elemento salir al menú archivo elementoSalir.addActionListener( new ActionListener() // clase interna anónima { // termina la aplicación cuando el usuario hace clic en elementoSalir public void actionPerformed( ActionEvent evento ) { System.exit( 0 ); // sale de la aplicación } // fin del método actionPerformed } // fin de la clase interna anónima ); // fin de la llamada a addActionListener JMenuBar barra = new JMenuBar(); // crea la barra de menús setJMenuBar( barra ); // agrega la barra de menús a la aplicación barra.add( menuArchivo ); // agrega el menú archivo a la barra de menús JMenu menuFormato = new JMenu( "Formato" ); // crea el menú formato menuFormato.setMnemonic( 'F' ); // establece el nemónico a F // arreglo que enlista la cadena colores String colores[] = { "Negro", "Azul", "Rojo", "Verde" }; JMenu menuColor = new JMenu( "Color" ); // crea el menú color menuColor.setMnemonic( 'C' ); // establece el nemónico a C // crea los elementos de menú de los botones de opción para los colores elementosColores = new JRadioButtonMenuItem[ colores.length ]; coloresButtonGroup = new ButtonGroup(); // administra los colores ManejadorElementos manejadorElementos = new ManejadorElementos(); // manejador para colores // crea los elementos de menú del botón de opción color for ( int cuenta = 0; cuenta < colores.length; cuenta++ ) { elementosColores[ cuenta ] = new JRadioButtonMenuItem( colores[ cuenta ] ); // crea elemento menuColor.add( elementosColores[ cuenta ] ); // agrega elemento al menú color coloresButtonGroup.add( elementosColores[ cuenta ] ); // lo agrega al grupo elementosColores[ cuenta ].addActionListener( manejadorElementos ); } // fin de for elementosColores[ 0 ].setSelected( true ); // selecciona el primer elemento de Color

Figura 22.5 | Objetos JMenu y nemónicos. (Parte 2 de 4).

www.elsolucionario.net

892

104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159

Capítulo 22

Componentes de la GUI: parte 2

menuFormato.add( menuColor ); // agrega el menú color al menú formato menuFormato.addSeparator(); // agrega un separador en el menú // arreglo que enlista los nombres de las fuentes String nombresFuentes[] = { "Serif", "Monospaced", "SansSerif" }; JMenu menuFuente = new JMenu( "Fuente" ); // crea el menú fuente menuFuente.setMnemonic( 'u' ); // establece el nemónico a u // crea elementos de menú de botones de opción para los nombres de las fuentes fuentes = new JRadioButtonMenuItem[ nombresFuentes.length ]; fuentesButtonGroup = new ButtonGroup(); // administra los nombres de las fuentes // crea elementos de menú de botones de opción de Fuente for ( int cuenta = 0; cuenta < fuentes.length; cuenta++ ) { fuentes[ cuenta ] = new JRadioButtonMenuItem( nombresFuentes[ cuenta ] ); menuFuente.add( fuentes[ cuenta ] ); // agrega fuente al menú fuente fuentesButtonGroup.add( fuentes[ cuenta ] ); // agrega al grupo de botones fuentes[ cuenta ].addActionListener( manejadorElementos ); // agrega el manejador } // fin de for fuentes[ 0 ].setSelected( true ); // selecciona el primer elemento del menú Fuente menuFuente.addSeparator(); // agrega barra separadora al menú fuente String nombresEstilos[] = { "Negrita", "Cursiva" }; // nombres de los estilos elementosEstilos = new JCheckBoxMenuItem[ nombresEstilos.length ]; ManejadorEstilos manejadorEstilos = new ManejadorEstilos(); // manejador de estilos // crea elementos de menú de la casilla de verificación de estilo for ( int cuenta = 0; cuenta < nombresEstilos.length; cuenta++ ) { elementosEstilos[ cuenta ] = new JCheckBoxMenuItem( nombresEstilos[ cuenta ] ); // para el estilo menuFuente.add( elementosEstilos[ cuenta ] ); // agrega al menú fuente elementosEstilos[ cuenta ].addItemListener( manejadorEstilos ); // manejador } // fin de for menuFormato.add( menuFuente ); // agrega el menú Fuente al menú Formato barra.add( menuFormato ); // agrega el menú Formato a la barra de menús // establece la etiqueta para mostrar el texto mostrarJLabel = new JLabel( "Texto de ejemplo", SwingConstants.CENTER ); mostrarJLabel.setForeground( valoresColores[ 0 ] ); mostrarJLabel.setFont( new Font( "'Serif", Font.PLAIN, 72 ) ); getContentPane().setBackground( Color.CYAN ); // establece el color de fondo add( mostrarJLabel, BorderLayout.CENTER ); // agrega mostrarJLabel } // fin del constructor de MarcoMenu // clase interna para manejar los eventos de acción de los elementos de menú private class ManejadorElementos implements ActionListener { // procesa las selecciones de color y fuente public void actionPerformed( ActionEvent evento ) { // procesa la selección del color

Figura 22.5 | Objetos JMenu y nemónicos. (Parte 3 de 4).

www.elsolucionario.net

22.4 Uso de menús con marcos

160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204

for ( int cuenta = 0; cuenta < elementosColores.length; cuenta++ ) { if ( elementosColores[ cuenta ].isSelected() ) { mostrarJLabel.setForeground( valoresColores[ cuenta ] ); break; } // fin de if } // fin de for // procesa la selección de fuente for ( int cuenta = 0; cuenta < fuentes.length; cuenta++ ) { if ( evento.getSource() == fuentes[ cuenta ] ) { mostrarJLabel.setFont( new Font( fuentes[ cuenta ].getText(), estilo, 72 ) ); } // fin de if } // fin de for repaint(); // vuelve a dibujar la aplicación } // fin del método actionPerformed } // fin de la clase ManejadorElementos // clase interna para manejar los eventos de los elementos de menú de las casillas de verificación private class ManejadorEstilos implements ItemListener { // procesa las selecciones de estilo de las fuentes public void itemStateChanged( ItemEvent e ) { estilo = 0; // inicializa el estilo // comprueba la selección de negrita if ( elementosEstilos[ 0 ].isSelected() ) estilo += Font.BOLD; // agrega negrita al estilo // comprueba la selección de cursiva if ( elementosEstilos[ 1 ].isSelected() ) estilo += Font.ITALIC; // agrega cursiva al estilo mostrarJLabel.setFont( new Font( mostrarJLabel.getFont().getName(), estilo, 72 ) ); repaint(); // vuelve a dibujar la aplicación } // fin del método itemStateChanged } // fin de la clase ManejadorEstilos } // fin de la clase MarcoMenu

Figura 22.5 | Objetos JMenu y nemónicos. (Parte 4 de 4). 1 2 3 4 5 6 7 8 9

// Fig. 22.6: PruebaMenu.java // Prueba de MarcoMenu. import javax.swing.JFrame; public class PruebaMenu { public static void main( String args[] ) { MarcoMenu marcoMenu = new MarcoMenu(); // crea objeto MarcoMenu

Figura 22.6 | Clase de prueba para MarcoMenu. (Parte 1 de 2).

www.elsolucionario.net

893

894

10 11 12 13 14

Capítulo 22

Componentes de la GUI: parte 2

marcoMenu.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); marcoMenu.setSize( 600, 200 ); // establece el tamaño del marco marcoMenu.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase PruebaMenu

Menú Barra de menús

Caracteres nemónicos

Submenú expandido Elementos de menú Línea separadora

Figura 22.6 | Clase de prueba para MarcoMenu. (Parte 2 de 2). En las líneas 38 a 76 se establece el menú Archivo y se adjunta a la barra de menús. El menú Archivo contiene un elemento de menú Acerca de…, el cual muestra un cuadro de diálogo de mensaje al ser seleccionado, y un elemento de menú Salir que puede seleccionarse para terminar la aplicación. En la línea 38 se crea el objeto JMenu y se pasa a su constructor la cadena "Archivo" como el nombre del menú. En la línea 39 se utiliza el método setMnemonic (heredado de la clase AbstractButton) para indicar que A es el nemónico para este menú. Al oprimir las teclas Alt y A se abre el menú, así como cuando se hace clic en el menú con el ratón. En la GUI, el carácter nemónico en el nombre del menú se muestra subrayado. (Vea las capturas de pantalla de la figura 22.6.) Archivo Nuevo Abrir... Cerrar

Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 22.3 Los nemónicos proporcionan un acceso rápido a los comandos de menú y de botón, por medio del teclado.

Observación de apariencia visual 22.4 Deben utilizarse distintos nemónicos para cada uno de los botones o elementos de menú. Generalmente, la primera letra en la etiqueta del elemento de menú o del botón es la que se utiliza como nemónico. Si varios botones o elementos de menú inician con la misma letra, seleccione la siguiente letra más prominente en el nombre (por ejemplo u se selecciona comúnmente para el elemento de menú Guardar como, del menú Archivo).

En las líneas 42 y 43 se crea el objeto JMenuItem elementoAcercaDe con el texto "Acerca de..." y se establece su nemónico en la letra c. (No se utiliza A porque esa letra es el nemónico del menú Archivo). Este elemento de menú se agrega al objeto menuArchivo en la línea 44 mediante el método add de JMenu. Para tener acceso al elemento Acerca de... a través del teclado, oprima la tecla Alt y la letra A para abrir el menú Archivo, después oprima c para seleccionar el elemento de menú Acerca de.... En las líneas 47 a 56 se crea un objeto ActionListener para procesar el evento de acción del objeto elementoAcercaDe. En las líneas 52 a 54 se muestra un cuadro de diálogo de mensaje. En la mayoría de los usos anteriores de showMessageDialog, el primer argumento ha sido null. El propósito del primer argumento es especificar la ventana padre para el cuadro de diálogo, la cual ayuda a determinar en dónde se mostrará el cuadro de diálogo. Si la ventana padre se especifica como null, el cuadro de diálogo aparece en el centro de la pantalla. En caso contrario, el cuadro de diálogo aparece centrado sobre la

www.elsolucionario.net

22.4 Uso de menús con marcos

895

ventana padre especificada. En este ejemplo, el programa especifica la ventana padre con PruebaMenu.this; la referencia this del objeto PruebaMenu. Al utilizar la referencia this en una clase interna, si se especifica la palabra this por sí sola, se hace referencia al objeto de la clase interna. Para hacer referencia a la referencia this del objeto de la clase externa, debe calificar a this con el nombre de la clase externa y un punto (.). Los cuadros de diálogo generalmente son modales. Un cuadro de diálogo modal no permite el acceso a ninguna de las otras ventanas en la aplicación, sino hasta que se cierre. Los cuadros de diálogo que se muestran con la clase JOptionPane son cuadros de diálogo modales. La clase JDialog puede usarse para crear sus propios cuadros de diálogo modales o no modales. En las líneas 59 a 72 se crea el elemento de menú elementoSalir, se establece su nemónico en S, se agrega a menuArchivo y se registra un objeto ActionListener que termina la aplicación cuando el usuario selecciona elementoSalir. En las líneas 74 a 76 se crea el objeto JMenuBar, se adjunta a la ventana de aplicación mediante el método setJMenuBar de JFrame y se utiliza el método add de JMenuBar para adjuntar el menuArchivo a la barra de menús JMenuBar.

Error común de programación 22.3 Si olvida establecer la barra de menús mediante el método setJMenuBar de JFrame, la barra de menús no se mostrará en el objeto JFrame. Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 22.5 Los menús generalmente aparecen de izquierda a derecha, en el orden en el que se agregan a un objeto JMenuBar.

En las líneas 78 a 79 se crea el menú llamado menuFormato y se establece su nemónico en F. En las líneas 84 y 85 se crea el menú llamado menuColor (éste será un submenú en el menú Formato) y se establece su nemónico en C. En la línea 88 se crea el arreglo JRadioButtonMenuItem llamado elementosColores, el cual hace referencia a los elementos de menú de menuColor. En la línea 89 se crea el objeto ButtonGroup llamado grupoColores, el cual asegura que sólo se seleccione uno de los elementos de menú del submenú Color en un momento dado. En la línea 90 se crea una instancia de la clase interna ManejadorElementos (declarada en las líneas 154 a 181) para responder a las selecciones del submenú Color y del submenú Fuente (que describiremos en breve). En la instrucción for de las líneas 93 a 100 se crea a cada uno de los objetos JRadioButtonMenuItem en el arreglo elementosColor, se agrega cada uno de los elementos de menú a menuColor y a grupoColores, y se registra el objeto ActionListener para cada elemento de menú. En la línea 102 se utiliza el método setSelected de AbstractButton para seleccionar el primer elemento en el arreglo elementosColor. En la línea 104 se agrega menuColor como un submenú de menuFormato. En la línea 105 se invoca al método addSeparator de JMenu para agregar una línea separadora horizontal al menú. Archivo Nuevo Abrir... Cerrar

Archivo Nuevo Abrir... Cerrar

Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 22.6 Un submenú se crea agregando a un menú como elemento de otro menú. Cuando el ratón se coloca sobre un submenú (o cuando se oprime el nemónico de ese submenú), éste se expande para mostrar sus elementos de menú.

Observación de apariencia visual 22.7 Pueden agregarse separadores a un menú para agrupar los elementos de menú en forma lógica.

Observación de apariencia visual 22.8 Puede agregarse cualquier componente de la GUI ligero (es decir, un componente de cualquier subclase de la clase JComponent) a un objeto JMenu o JMenuBar.

En las líneas 108 a 126 se crean el submenú Fuente y varios objetos JRadioButtonMenuItem, y se selecciona el primer elemento del arreglo JRadioButtonMenuItem llamado fuentes. En la línea 129 se crea un arreglo JCheckBoxMenuItem para representar a los elementos de menú que especifican los estilos negrita y cursiva para las fuentes. En la línea 130 se crea una instancia de la clase interna ManejadorEstilos (declarada en las líneas 184 a 203) para responder a los eventos de JCheckBoxMenuItem. La instrucción for de las líneas 133 a 139 crea a cada objeto JCheckBoxMenuItem, agrega cada uno de los elementos de menú a menuFuente y registra el objeto

www.elsolucionario.net

896

Capítulo 22

Componentes de la GUI: parte 2

ItemListener para cada elemento de menú. En la línea 141 se agrega menuFuente como un submenú de menuFormato. En la línea 142 se agrega el menuFormato a la barra (de menús). En las líneas 145 a 147 se crea un objeto JLabel para el cual los elementos del menú Formato controlan

el tipo de letra, su color y estilo. El color inicial de primer plano se establece con el primer elemento del arreglo valoresColor (Color.BLACK) mediante la invocación al método setForeground de JComponent, y el tipo de letra inicial se establece en Serif con estilo PLAIN y tamaño de 72 puntos. En la línea 149 se establece el color de fondo del panel de contenido de la ventana en cyan, y en la línea 150 se adjunta el objeto JLabel a la región CENTER del esquema BorderLayout del panel de contenido. El método actionPerformed de ManejadorElementos (líneas 157 a 180) utiliza dos instrucciones for para determinar cuál fue el elemento de menú de fuente o de color que generó el evento, y establece la fuente o el color del objeto JLabel llamado mostrarEtiqueta, respectivamente. La condición if de la línea 162 utiliza el método isSelected de AbstractButton para determinar cuál fue el objeto JRadioButtonMenuItem seleccionado. La condición if de la línea 172 invoca al método getSource del objeto evento para obtener una referencia al objeto JRadioButtonMenuItem que generó el evento. En la línea 175 se utiliza el método getText de AbstractButton para obtener el nombre del tipo de letra del elemento de menú. El programa llama al método itemStateChanged de ManejadorEstilo (líneas 187 a 202) si el usuario selecciona un objeto JCheckBoxMenuItem en el menuFuente. En las líneas 192 y 196 se determina si uno o ambos objetos JCheckBoxMenuItem se seleccionan, y se utiliza su estado combinado para determinar el nuevo estilo de fuente.

22.5 JPopupMenu

Muchas de las aplicaciones de computadora de la actualidad proporcionan lo que se conoce como menús contextuales sensibles al contexto. En Swing, tales menús se crean con la clase JPopupMenu (una subclase de JComponent). Estos menús proporcionan opciones específicas al componente para el cual se generó el evento de desencadenamiento del menú contextual. En la mayoría de los sistemas, este evento de desencadenamiento ocurre cuando el usuario oprime y suelta el botón derecho del ratón. Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 22.9 El evento de desencadenamiento del menú contextual es específico para cada plataforma. En la mayoría de las plataformas que utilizan un ratón con varios botones, el evento de desencadenamiento del menú contextual ocurre cuando el usuario hace clic con el botón derecho del ratón en un componente que tiene soporte para un menú contextual.

La aplicación de las figuras 22.7 a 22.8 crea un objeto JPopupMenu, el cual permite al usuario seleccionar uno de tres colores y cambiar el color de fondo de la ventana. Cuando el usuario hace clic con el botón derecho del ratón en el fondo de la ventana PruebaContextual, aparece un objeto JPopupMenu, el cual contiene unos colores. Si el usuario hace clic en un objeto JRadioButtonMenuItem para seleccionar un color, el método actionPerformed de ManejadorElementos cambia el color de fondo del panel de contenido de la ventana.

1 2 3 4 5 6 7 8 9 10 11 12 13 14

// Fig. 22.7: MarcoContextual.java // Demostración de los objetos JPopupMenu. import java.awt.Color; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import javax.swing.JFrame; import javax.swing.JRadioButtonMenuItem; import javax.swing.JPopupMenu; import javax.swing.ButtonGroup; public class MarcoContextual extends JFrame {

Figura 22.7 | Objeto JPopupMenu para seleccionar colores. (Parte 1 de 3).

www.elsolucionario.net

22.5

15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68

JPopupMenu

897

private JRadioButtonMenuItem elementos[]; // contiene los elementos para los colores private final Color valoresColores[] = { Color.BLUE, Color.YELLOW, Color.RED }; // colores a utilizar private JPopupMenu menuContextual; // permite al usuario seleccionar el color // el constructor sin argumentos establece la GUI public MarcoContextual() { super( "Uso de objetos JPopupMenu" ); ManejadorElementos manejador = new ManejadorElementos(); // manejador para los elementos de menú String colores[] = { "Azul", "Amarillo", "Rojo" }; // arreglo de colores ButtonGroup grupoColores = new ButtonGroup(); // administra los elementos de colores menuContextual = new JPopupMenu(); // crea el menú contextual elementos = new JRadioButtonMenuItem[ 3 ]; // elementos para seleccionar el color // construye elemento de menú, lo agrega al menú contextual, habilita el manejo de eventos for ( int cuenta = 0; cuenta < elementos.length; cuenta++ ) { elementos[ cuenta ] = new JRadioButtonMenuItem( colores[ cuenta ] ); menuContextual.add( elementos[ cuenta ] ); // agrega elemento al menú contextual grupoColores.add( elementos[ cuenta ] ); // agrega elemento al grupo de botones elementos[ cuenta ].addActionListener( manejador ); // agrega el manejador } // fin de for setBackground( Color.WHITE ); // establece el color de fondo en blanco // declara un objeto MouseListener para que la ventana muestre el menú contextual addMouseListener( new MouseAdapter() // clase interna anónima { // maneja el evento de oprimir el botón del ratón public void mousePressed( MouseEvent evento ) { checkForTriggerEvent( evento ); // comprueba el desencadenador } // fin del método mousePressed // maneja el evento de liberación del botón del ratón public void mouseReleased( MouseEvent evento ) { checkForTriggerEvent( evento ); // comprueba el desencadenador } // fin del método mouseReleased // determina si el evento debe desencadenar el menú contextual private void checkForTriggerEvent( MouseEvent evento ) { if ( evento.isPopupTrigger() ) menuContextual.show( evento.getComponent(), evento.getX(), evento.getY() ); } // fin del método checkForTriggerEvent } // fin de la clase interna anónima ); // fin de la llamada a addMouseListener

Figura 22.7 | Objeto JPopupMenu para seleccionar colores. (Parte 2 de 3).

www.elsolucionario.net

898

69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88

Capítulo 22

Componentes de la GUI: parte 2

} // fin del constructor de MarcoContextual // clase interna privada para manejar los eventos de los elementos de menú private class ManejadorElementos implements ActionListener { // procesa las selecciones de los elementos de menú public void actionPerformed( ActionEvent evento ) { // determina cuál elemento de menú se seleccionó for ( int i = 0; i < elementos.length; i++ ) { if ( evento.getSource() == elementos[ i ] ) { getContentPane().setBackground( valoresColores[ i ] ); return; } // fin de if } // fin de for } // fin del método actionPerformed } // fin de la clase interna privada ManejadorElementos } // fin de la clase MarcoContextual

Figura 22.7 | Objeto JPopupMenu para seleccionar colores. (Parte 3 de 3).

En la línea 25 del constructor de MarcoContextual (líneas 21 a 69) se crea una instancia de la clase Mane(declarada en las líneas 72 a 87), la cual procesará los eventos de los elementos de menú en el menú contextual. En la línea 29 se crea el objeto JPopupMenu. La instrucción for (líneas 33 a 39) crea un objeto JRadioButtonMenuItem (línea 35), lo agrega al objeto menuContextual (línea 36), agrega este objeto al objeto ButtonGroup llamado grupoColores (línea 37) para mantener sólo un objeto JRadioButtonMenuItem seleccionado a la vez, y registra su objeto ActionListener (línea 38). En la línea 41 se establece el fondo inicial en blanco, invocando al método setBackground. En las líneas 44 a 68 se registra un objeto MouseListener para manejar los eventos de ratón de la ventana de aplicación. Los métodos mousePressed (líneas 49 a 52) y mouseReleased (líneas 55 a 58) comprueban el evento de desencadenamiento del menú contextual. Cada método llama al método utilitario privado checkForTriggerEvent (líneas 61 a 66) para determinar si ocurrió el evento de desencadenamiento del menú contextual. De ser así, el método isPopupTrigger de MouseEvent devuelve true, y el método show de JPopupMenu muestra el objeto JPopupMenu. El primer argumento del método show especifica el componente de origen, cuya posición ayuda a determinar en dónde aparecerá objeto JPopupMenu en la pantalla. Los últimos dos argumentos son las coordenadas x-y (medidas desde la esquina superior izquierda del componente de origen) en donde debe aparecer el objeto JPopupMenu. jadorElementos

1 2 3 4 5 6 7 8 9 10 11 12 13 14

// Fig. 22.8: PruebaContextual.java // Prueba de MarcoContextual. import javax.swing.JFrame; public class PruebaContextual { public static void main( String args[] ) { MarcoContextual marcoContextual = new MarcoContextual(); // crea MarcoContextual marcoContextual.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); marcoContextual.setSize( 300, 200 ); // establece el tamaño del marco marcoContextual.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase PruebaContextual

Figura 22.8 | Clase de prueba para MarcoContextual. (Parte 1 de 2).

www.elsolucionario.net

22.6 Apariencia visual adaptable

899

Figura 22.8 | Clase de prueba para MarcoContextual. (Parte 2 de 2).

Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 22.10 Para mostrar un objeto JPopupMenu para el evento de desencadenamiento de menú contextual de varios componentes de la GUI, se deben registrar manejadores de eventos de ratón para cada uno de esos componentes de la GUI.

Cuando el usuario selecciona un elemento del menú contextual, el método actionPerformed de la clase (líneas 75 a 86) determina cuál objeto JRadioButtonMenuItem fue seleccionado por el usuario y establece el color de fondo del panel de contenido de la ventana. ManejadorElementos

22.6 Apariencia visual adaptable Un programa que utiliza componentes de la GUI del Abstract Window Toolkit de Java (paquete java.awt) asume la apariencia visual de la plataforma en la que se ejecute. Una aplicación de Java ejecutándose en una Macintosh tiene la misma apariencia que las demás aplicaciones que se ejecutan en la Macintosh. Una aplicación de Java ejecutándose en Microsoft Windows tiene la misma apariencia que las demás aplicaciones que se ejecutan en Microsoft Windows. Una aplicación de Java ejecutándose en una plataforma UNIX tiene la misma apariencia que las demás aplicaciones que se ejecutan en esa plataforma UNIX. Esto puede ser conveniente, ya que permite a los usuarios del programa utilizar, en cada plataforma, los componentes de la GUI con los que ya están familiarizados. Sin embargo, esto también introduce algunas cuestiones interesantes, relacionadas con la portabilidad.

Tip de portabilidad 22.1 Los componentes de la GUI en cada plataforma tienen distinta apariencia, lo cual puede requerir de distintas cantidades de espacio para mostrarse en pantalla. Esto podría cambiar la distribución y alineación de los componentes de la GUI.

Tip de portabilidad 22.2 Los componentes de la GUI en cada plataforma tienen distinta funcionalidad predeterminada (por ejemplo, algunas plataformas permiten que un botón con el foco se “oprima” mediante la barra de espaciamiento, mientras que otras no).

Los componentes de la GUI ligeros de Swing eliminan muchas de estas cuestiones, al proporcionar una funcionalidad uniforme entre plataformas, y al definir una apariencia visual uniforme entre plataformas (a la que se le conoce como la apariencia visual metálica). Swing también proporciona la flexibilidad de personalizar la apariencia visual, para que un programa tenga la apariencia visual al estilo Microsoft Windows (en sistemas Windows), al estilo Motif (UNIX) (en todas las plataformas) o al estilo Mac (sistemas Mac). La aplicación de las figuras 22.9 y 22.10 demuestra cómo cambiar la apariencia visual de una GUI de Swing. La aplicación crea varios componentes de la GUI, por lo que usted podrá ver el cambio en la apariencia visual de varios componentes de la GUI al mismo tiempo. La primera ventana de salida muestra la apariencia visual metálica estándar, la segunda ventana de salida muestra la apariencia visual Motif y la tercera ventana muestra la apariencia visual Windows.

www.elsolucionario.net

900

Capítulo 22

Componentes de la GUI: parte 2

Todos los componentes de la GUI y el manejo de eventos de este ejemplo se han descrito anteriormente, por lo que ahora nos concentraremos en el mecanismo para cambiar la apariencia visual. La clase UIManager (paquete javax.swing) contiene la clase anidada LookAndFeelInfo (una clase public static) que mantiene la información acerca de una apariencia visual. En la línea 22 se declara un arreglo de tipo UIManager.LookAndFeelInfo (observe la sintaxis utilizada para identificar a la clase interna LookAndFeelInfo). En la línea 68 se usa el método static getInstalledLookAndFeels de UIManager para obtener el arreglo de objetos UIManager.LookAndFeelInfo que describe cada una de las apariencias visuales disponibles en su sistema.

Tip de rendimiento 22.2 Cada apariencia visual se representa mediante una clase de Java. El método getInstalledLookAndFeels de UIManager no carga a cada una de esas clases. En vez de ello, proporciona los nombres de las clases de apariencia visual disponibles, de manera que pueda seleccionarse una de ellas (se supone que una vez, al inicio del programa). Esto reduce la sobrecarga que se genera al cargar clases adicionales que el programa no va a utilizar.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

// Fig. 22.9: MarcoAparienciaVisual.java // Cambio de la apariencia visual. import java.awt.GridLayout; import java.awt.BorderLayout; import java.awt.event.ItemListener; import java.awt.event.ItemEvent; import javax.swing.JFrame; import javax.swing.UIManager; import javax.swing.JRadioButton; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JComboBox; import javax.swing.JPanel; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; public class MarcoAparienciaVisual extends JFrame { // nombres de las apariencias visuales private final String cadenas[] = { "Metal", "Motif", "Windows" }; private UIManager.LookAndFeelInfo apariencias[]; // apariencias visuales private JRadioButton opcion[]; // botones de opción para seleccionar la apariencia visual private ButtonGroup grupo; // grupo para los botones de opción private JButton boton; // muestra la apariencia del botón private JLabel etiqueta; // muestra la apariencia de la etiqueta private JComboBox cuadroComb; // muestra la apariencia del cuadro combinado // establece la GUI public MarcoAparienciaVisual() { super( "Demo de apariencia visual" ); JPanel panelNorte = new JPanel(); // crea panel norte panelNorte.setLayout( new GridLayout( 3, 1, 0, 5 ) ); etiqueta = new JLabel( "Esta es una apariencia visual metalica", SwingConstants.CENTER ); // crea etiqueta panelNorte.add( etiqueta ); // agrega etiqueta al panel

Figura 22.9 | Apariencia visual de una GUI basada en Swing. (Parte 1 de 3).

www.elsolucionario.net

22.6 Apariencia visual adaptable

41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98

boton = new JButton( "JButton" ); // crea botón panelNorte.add( boton ); // agrega botón al panel cuadroComb = new JComboBox( cadenas ); // crea cuadro combinado panelNorte.add( cuadroComb ); // agrega cuadro combinado al panel // crea arreglo para los botones de opción opcion = new JRadioButton[ cadenas.length ]; JPanel panelSur = new JPanel(); // crea panel sur panelSur.setLayout( new GridLayout( 1, opcion.length ) ); grupo = new ButtonGroup(); // grupo de botones para las apariencias visuales ManejadorElementos manejador = new ManejadorElementos(); // manejador de apariencia visual for ( int cuenta = 0; cuenta < opcion.length; cuenta++ ) { opcion[ cuenta ] = new JRadioButton( cadenas[ cuenta ] ); opcion[ cuenta ].addItemListener( manejador ); // agrega el manejador grupo.add( opcion[ cuenta ] ); // agrega botón de opción al grupo panelSur.add( opcion[ cuenta ] ); // agrega botón de opción al panel } // fin de for add( panelNorte, BorderLayout.NORTH ); // agrega panel norte add( panelSur, BorderLayout.SOUTH ); // agrega panel sur // obtiene la información de la apariencia visual instalada apariencias = UIManager.getInstalledLookAndFeels(); opcion[ 0 ].setSelected( true ); // establece la selección predeterminada } // fin del constructor de MarcoAparienciaVisual // usa UIManager para cambiar la apariencia visual de la GUI private void cambiarAparienciaVisual( int valor ) { try // cambia la apariencia visual { // establece la apariencia visual para esta aplicación UIManager.setLookAndFeel( apariencias[ valor ].getClassName() ); // actualiza los componentes en esta aplicación SwingUtilities.updateComponentTreeUI( this ); } // fin de try catch ( Exception excepcion ) { excepcion.printStackTrace(); } // fin de catch } // fin del método cambiarAparienciaVisual // clase interna privada para manejar los eventos de los botones de opción private class ManejadorElementos implements ItemListener { // procesa la selección de apariencia visual del usuario public void itemStateChanged( ItemEvent evento ) { for ( int cuenta = 0; cuenta < opcion.length; cuenta++ ) { if ( opcion[ cuenta ].isSelected() ) {

Figura 22.9 | Apariencia visual de una GUI basada en Swing. (Parte 2 de 3).

www.elsolucionario.net

901

902

Capítulo 22

99 100 101 102 103 104 105 106 107

} } // } // fin } // fin de

} // fin de la

Componentes de la GUI: parte 2

etiqueta.setText( String.format( "Esta es una apariencia visual %s", cadenas[ cuenta ] ) ); cuadroComb.setSelectedIndex( cuenta ); // establece el índice del cuadro combinado cambiarAparienciaVisual( cuenta ); // cambia la apariencia visual // fin de if fin de for del método itemStateChanged la clase interna privada ManejadorElementos clase MarcoAparienciaVisual

Figura 22.9 | Apariencia visual de una GUI basada en Swing. (Parte 3 de 3).

1 2 3 4 5 6 7 8 9 10 11 12 13 14

// Fig. 22.10: PruebaContextual.java // Prueba de MarcoContextual. import javax.swing.JFrame; public class PruebaContextual { public static void main( String args[] ) { MarcoContextual marcoContextual = new MarcoContextual(); // crea MarcoContextual marcoContextual.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); marcoContextual.setSize( 300, 200 ); // establece el tamaño del marco marcoContextual.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase PruebaContextual

Figura 22.10 | Clase de prueba para MarcoContextual.

Nuestro método utilitario cambiarAparienciaVisual (líneas 73 a 87) es llamado por el manejador de eventos para los objetos JRadioButton que se encuentran en la parte inferior de la interfaz de usuario. El manejador de eventos (declarado en la clase interna private ManejadorElementos en las líneas 90 a 106) le pasa un valor entero que representa el elemento en el arreglo apariencias que deberá utilizarse para cambiar la apariencia visual. En la línea 78 se invoca el método static setLookAndFeel de UIManager para cambiar

www.elsolucionario.net

22.7

JDesktopane

y JInternalFrame

903

la apariencia visual. El método getClassName de la clase UIManager.LookAndFeelInfo determina el nombre de la clase de apariencia visual que corresponde al objeto UIManager.LookAndFeelInfo. Si la clase de apariencia visual no se ha cargado ya, se cargará como parte de la llamada a setLookAndFeel. En la línea 81 se invoca el método static updateComponentTreeUI de la clase SwingUtilities (paquete javax.swing) para cambiar la apariencia visual de todos los componentes de GUI adjuntos a su argumento (la instancia this de nuestra clase de aplicación DemoAparienciaVisual) a la nueva apariencia visual.

22.7 JDesktopPane y JInternalFrame

Muchas de las aplicaciones de hoy en día utilizan una interfaz de múltiples documentos (MDI): una ventana principal (a la que se le conoce comúnmente como la ventana padre) que contiene otras ventanas (a las que se les conoce comúnmente como ventanas hijas), para administrar varios documentos abiertos que se procesan en paralelo. Por ejemplo, muchos programas de correo electrónico le permiten tener varias ventanas abiertas al mismo tiempo, para que usted pueda componer o leer varios mensajes de correo electrónico de manera simultánea. De manera similar, muchos procesadores de palabras permiten al usuario abrir varios documentos en ventanas separadas, para que el usuario pueda alternar entre los documentos sin tener que cerrar el documento actual para abrir otro. La aplicación de las figuras 22.11 y 22.12 demuestra el uso de las clases JDesktopPane y JInternalFrame de Swing para implementar interfaces de múltiples documentos. En las líneas 27 a 33 se crean objetos JMenuBar, JMenu y JMenuItem, se agrega el objeto JMenuItem al objeto JMenu, se agrega el objeto JMenu al objeto JMenuBar y se establece el objeto JMenuBar para la ventana de aplicación. Cuando el usuario selecciona el objeto JMenuItem nuevoMarco, la aplicación crea y muestra un nuevo objeto JInternalFrame que contiene una imagen.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

// Fig. 22.11: MarcoEscritorio.java // Demostración de JDesktopPane. import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Graphics; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import java.util.Random; import javax.swing.JFrame; import javax.swing.JDesktopPane; import javax.swing.JMenuBar; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JInternalFrame; import javax.swing.JPanel; import javax.swing.ImageIcon; public class MarcoEscritorio extends JFrame { private JDesktopPane elEscritorio; // establece la GUI public MarcoEscritorio() { super( "Uso de JDesktopPane" ); JMenuBar barra = new JMenuBar(); // crea la barra de menús JMenu menuAgregar = new JMenu( "Agregar" ); // crea el menú Agregar JMenuItem nuevoMarco = new JMenuItem( "Marco interno" ); menuAgregar.add( nuevoMarco ); // agrega nuevo elemento marco al menú Agregar

Figura 22.11 | Interfaz de múltiples documentos. (Parte 1 de 2).

www.elsolucionario.net

904

32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90

Capítulo 22

Componentes de la GUI: parte 2

barra.add( menuAgregar ); // agrega el menú Agregar a la barra de menús setJMenuBar( barra ); // establece la barra de menús para esta aplicación elEscritorio = new JDesktopPane(); // crea el panel de escritorio add( elEscritorio ); // agrega el panel de escritorio al marco // establece componente de escucha para el elemento de menú nuevoMarco nuevoMarco.addActionListener( new ActionListener() // clase interna anónima { // muestra la nueva ventana interna public void actionPerformed( ActionEvent evento ) { // crea el marco interno JInternalFrame marco = new JInternalFrame( "Marco interno", true, true, true, true ); MiJPanel panel = new MiJPanel(); // crea nuevo panel marco.add( panel, BorderLayout.CENTER ); // agrega el panel marco.pack(); // establece marco interno al tamaño del contenido elEscritorio.add( marco ); // adjunta marco interno marco.setVisible( true ); // muestra marco interno } // fin del método actionPerformed } // fin de la clase interna anónima ); // fin de la llamada a addActionListener } // fin del constructor de MarcoEscritorio } // fin de la clase MarcoEscritorio // clase para mostrar un objeto ImageIcon en un panel class MiJPanel extends JPanel { private static Random generador = new Random(); private ImageIcon imagen; // imagen a mostrar private String[] imagenes = { "floresamarillas.png", "floresmoradas.png", "floresrojas.png", "floresrojas2.png", "floreslavanda.png" }; // carga la imagen public MiJPanel() { int numeroAleatorio = generador.nextInt( 5 ); imagen = new ImageIcon( imagenes[ numeroAleatorio ] ); // establece el icono } // fin del constructor de MiJPanel // muestra el objeto imageIcon en el panel public void paintComponent( Graphics g ) { super.paintComponent( g ); imagen.paintIcon( this, g, 0, 0 ); // muestra el icono } // fin del método paintComponent // devuelve las medidas de la imagen public Dimension getPreferredSize() { return new Dimension( imagen.getIconWidth(), imagen.getIconHeight() ); } // fin del método getPreferredSize } // fin de la clase MiJPanel

Figura 22.11 | Interfaz de múltiples documentos. (Parte 2 de 2).

www.elsolucionario.net

22.7

1 2 3 4 5 6 7 8 9 10 11 12 13 14

JDesktopPane

y JInternalFrame

// Fig. 22.12: PruebaEscritorio.java // Demostración de MarcoEscritorio. import javax.swing.JFrame; public class PruebaEscritorio { public static void main( String args[] ) { MarcoEscritorio marcoEscritorio = new MarcoEscritorio(); marcoEscritorio.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); marcoEscritorio.setSize( 600, 480 ); // establece el tamaño del marco marcoEscritorio.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase PruebaEscritorio Marcos internos

Minimizar

Marcos internos minimizados

Maximizar

Cerrar

Coloque el ratón sobre cualquier esquina de una ventana hija para cambiar su tamaño (si se permite)

Marco interno maximizado

Figura 22.12 | Clase de prueba para MarcoEscritorio.

www.elsolucionario.net

905

906

Capítulo 22

Componentes de la GUI: parte 2

En la línea 35 se asigna la variable JDesktopPane (paquete javax.swing) llamada elEscritorio a un nuevo objeto JDesktopPane que se utilizará para administrar las ventanas hijas JInternalFrame. En la línea 36 se agrega el objeto JDesktopPane al objeto JFrame. De manera predeterminada, el objeto JDesktopPane se agrega al centro del esquema BorderLayout del panel de contenido, por lo que el objeto JDesktopPane se expande para rellenar toda la ventana de aplicación. En las líneas 39 a 58 se registra un objeto ActionListener para manejar el evento cuando el usuario selecciona el elemento de menú nuevoMarco. Cuando ocurre el evento, el método actionPerformed (líneas 44 a 56) crea un objeto JInternalFrame en las líneas 47 y 48. El constructor de JInternalFrame que se utiliza aquí requiere cinco argumentos: una cadena para la barra de título de la ventana interna, un valor boolean que indique si el usuario puede reajustar el tamaño del marco interno, un valor boolean que indique si el usuario puede cerrar el marco interno, un valor boolean que indique si el usuario puede maximizar el marco interno y un valor boolean que indique si el usuario puede minimizar el marco interno. Para cada uno de los argumentos boolean, un valor de true indica que la operación debe permitirse (como se da el caso aquí). Al igual que con los objetos JFrame y JApplet, un objeto JInternalFrame tiene un panel de contenido, al cual pueden adjuntarse componentes de la GUI. En la línea 50 se crea una instancia de nuestra clase MiJPanel (declarada en las líneas 63 a 90), la cual se agrega al objeto JInternalFrame en la línea 51. En la línea 52 se utiliza el método pack de JInternalFrame para establecer el tamaño de la ventana hija. El método pack utiliza los tamaños preferidos de los componentes para determinar el tamaño de la ventana. La clase MiJPanel declara el método getPreferredSize (líneas 85 a 89) para especificar el tamaño preferido del panel, para que lo use el método pack. En la línea 54 se agrega el objeto JInternalFrame al objeto JDesktopPane, y en la línea 55 se muestra el objeto JInternalFrame. Las clases JInternalFrame y JDesktopPane proporcionan muchos métodos para administrar ventanas hijas. Vea la documentación de la API en línea acerca de JInternalFrame y JDesktopPane, para obtener listas completas de estos métodos: java.sun.com/javase/6/docs/api/javax/swing/JInternalFrame.html java.sun.com/javase/6/docs/api/javax/swing/JDesktopPane.html

22.8 JTabbedPane

Un objeto JTabbedPane ordena los componentes de la GUI en capas, en donde sólo una capa está visible en un momento dado. Los usuarios acceden a cada una de las capas mediante una ficha; algo muy parecido a las carpetas en un archivero. Cuando el usuario hace clic en una ficha, se muestra la capa apropiada. Las fichas aparecen en la parte superior de manera predeterminada, pero también pueden colocarse a la izquierda, derecha o en la parte inferior del objeto JTabbedPane. Puede colocarse cualquier componente en una ficha. Si el componente es un contenedor como un panel, puede utilizar cualquier administrador de esquemas para distribuir varios componentes en esa ficha. La clase JTabbedPane es una subclase de JComponent. La aplicación de las figuras 22.13 y 22.14 crea un panel con tres fichas. Cada ficha muestra uno de los objetos JPanel: panel1, panel2 o panel3.

1 2 3 4 5 6 7 8 9 10 11 12 13 14

// Fig. 22.13: MarcoJTabbedPane.java // Demostración de JTabbedPane. import java.awt.BorderLayout; import java.awt.Color; import javax.swing.JFrame; import javax.swing.JTabbedPane; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JButton; import javax.swing.SwingConstants; public class MarcoJTabbedPane extends JFrame { // establece la GUI

Figura 22.13 | Uso de un objeto JTabbedPane para organizar los componentes de una GUI. (Parte 1 de 2).

www.elsolucionario.net

22.8

15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

JTabbedPane

907

public MarcoJTabbedPane() { super( "Demo de JTabbedPane " ); JTabbedPane panelFichas = new JTabbedPane(); // crea objeto JTabbedPane // establece pane11 y lo agrega al objeto JTabbedPane JLabel etiqueta1 = new JLabel( "panel uno", SwingConstants.CENTER ); JPanel panel1 = new JPanel(); // crea el primer panel panel1.add( etiqueta1 ); // agrega etiqueta al panel panelFichas.addTab( "Ficha uno", null, panel1, "Primer panel" ); // establece panel2 y lo agrega al objeto JTabbedPane JLabel etiqueta2 = new JLabel( "panel dos", SwingConstants.CENTER ); JPanel panel2 = new JPanel(); // crea el segundo panel panel2.setBackground( Color.YELLOW ); // establece el color de fondo en amarillo panel2.add( etiqueta2 ); // agrega etiqueta al panel panelFichas.addTab( "Ficha dos", null, panel2, "Segundo panel" ); // establece panel3 y lo agrega al objeto JTabbedPane JLabel etiqueta3 = new JLabel( "panel tres" ); JPanel panel3 = new JPanel(); // crea el tercer panel panel3.setLayout( new BorderLayout() ); // usa esquema Borderlayout panel3.add( new JButton( "Norte" ), BorderLayout.NORTH ); panel3.add( new JButton( "Oeste" ), BorderLayout.WEST ); panel3.add( new JButton( "Este" ), BorderLayout.EAST ); panel3.add( new JButton( "Sur" ), BorderLayout.SOUTH ); panel3.add( etiqueta3, BorderLayout.CENTER ); panelFichas.addTab( "Ficha tres", null, panel3, "Tercer panel" ); add( panelFichas ); // agrega objeto JTabbedPane al marco } // fin del constructor de MarcoJTabbedPane } // fin de la clase MarcoJTabbedPane

Figura 22.13 | Uso de un objeto JTabbedPane para organizar los componentes de una GUI. (Parte 2 de 2). El constructor (líneas 15 a 46) crea la GUI. En la línea 19 se crea un objeto JTabbedPane vacío con la configuración predeterminada (es decir, fichas en la parte superior). Si las fichas no se ajustan en una línea, pasarán a la siguiente para formar líneas adicionales de fichas. A continuación, el constructor crea los objetos JPanel panel1, panel2 y panel3, junto con sus componentes de la GUI. A medida que configuramos cada panel, lo agregamos al objeto panelConFichas utilizando el método addTab de JTabbedPane con cuatro argumentos. El primer argumento es una cadena que especifica el título de la ficha. El segundo es una referencia Icon que especifica un icono a mostrar en la ficha. Si el objeto Icon es una referencia null, no se muestra una imagen. El tercer argumento es una referencia Component que representa el componente de la GUI a mostrar cuando el usuario hace clic en la ficha. El último argumento es una cadena que especifica el cuadro de información sobre herramientas de la ficha. Por ejemplo, en la línea 25 se agrega el objeto JPanel panel1 al objeto panelFichas con el título "Ficha uno" y la información sobre herramientas "Primer panel". Los objetos JPanel panel2 y panel3 se agregan a panelFichas en las líneas 32 y 43. Para ver cada una de las fichas, haga clic en la ficha deseada con el ratón o utilice las teclas de dirección para avanzar por cada una de las fichas.

1 2 3 4

// Fig. 22.14: DemoJTabbedPane.java // Demostración de JTabbedPane. import javax.swing.JFrame;

Figura 22.14 | Clase de prueba para MarcoJTabbedPane. (Parte 1 de 2).

www.elsolucionario.net

908

5 6 7 8 9 10 11 12 13 14

Capítulo 22

Componentes de la GUI: parte 2

public class DemoJTabbedPane { public static void main( String args[] ) { MarcoJTabbedPane marcoPanelFichas = new MarcoJTabbedPane(); marcoPanelFichas.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); marcoPanelFichas.setSize( 250, 200 ); // establece el tamaño del marco marcoPanelFichas.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase DemoJTabbedPane

Figura 22.14 | Clase de prueba para MarcoJTabbedPane. (Parte 2 de 2).

22.9 Administradores de esquemas: BoxLayout y GridBagLayout

En el capítulo 11 presentamos tres administradores de esquemas: FlowLayout, BorderLayout y GridLayout. En esta sección presentamos dos administradores de esquemas adicionales (los cuales se sintetizan en la figura 22.15). En los siguientes ejemplos, hablaremos sobre estos administradores de esquemas.

Administrador de esquemas

Descripción

BoxLayout

Un administrador de esquemas que permite ordenar los componentes de la GUI de izquierda a derecha, o de arriba hacia abajo, en un contenedor. La clase Box declara un contenedor con BoxLayout como su administrador de esquemas predeterminado, y proporciona métodos estáticos para crear un objeto Box con un esquema BoxLayout horizontal o vertical.

GridBagLayout

Un administrador de esquemas similar a GridLayout. A diferencia de GridLael tamaño de cada componente puede variar y pueden agregarse componentes en cualquier orden.

yout,

Figura 22.15 | Administradores de esquemas adicionales.

Administrador de esquemas BoxLayout El administrador de esquemas BoxLayout (en el paquete javax.swing) ordena los componentes de la GUI en forma horizontal a lo largo del eje x, o en forma vertical a lo largo del eje y de un contenedor. La aplicación de las figuras 22.16 a 22.17 demuestra el uso del esquema BoxLayout y la clase contenedora Box que utiliza a BoxLayout como su administrador de esquemas predeterminado. En las líneas 19 a 22 se crean contenedores Box. Las referencias horizontal1 y horizontal2 se inicializan con el método estático createHorizontalBox de Box, el cual devuelve un contenedor Box con un esquema BoxLayout horizontal en el que los componentes de la GUI se ordenan de izquierda a derecha. Las variables vertical1 y vertical2 se inicializan con el método estático createVerticalBox de Box, el cual devuelve refe-

www.elsolucionario.net

22.9

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

Administradores de esquemas: BoxLayout y GridBagLayout

// Fig. 22.16: MarcoBoxLayout.java // Demostración de BoxLayout. import java.awt.Dimension; import javax.swing.JFrame; import javax.swing.Box; import javax.swing.JButton; import javax.swing.BoxLayout; import javax.swing.JPanel; import javax.swing.JTabbedPane; public class MarcoBoxLayout extends JFrame { // establece la GUI public MarcoBoxLayout() { super( "Demostración de BoxLayout" ); // crea contenedores Box con BoxLayout Box horizontal1 = Box.createHorizontalBox(); Box vertical1 = Box.createVerticalBox(); Box horizontal2 = Box.createHorizontalBox(); Box vertical2 = Box.createVerticalBox(); final int TAMANIO = 3; // número de botones en cada objeto Box // agrega botones al objeto Box horizontal1 for ( int cuenta = 0; cuenta < TAMANIO; cuenta++ ) horizontal1.add( new JButton( "Boton " + cuenta ) ); // crea montante y agrega botones al objeto Box vertical1 for ( int cuenta = 0; cuenta < TAMANIO; cuenta++ ) { vertical1.add( Box.createVerticalStrut( 25 ) ); vertical1.add( new JButton( "Boton " + cuenta ) ); } // fin de for // crea pegamento horizontal y agrega botones al objeto Box horizontal2 for ( int cuenta = 0; cuenta < TAMANIO; cuenta++ ) { horizontal2.add( Box.createHorizontalGlue() ); horizontal2.add( new JButton( "Boton " + cuenta ) ); } // fin de for // crea un área rígida y agrega botones al objeto Box vertical2 for ( int cuenta = 0; cuenta < TAMANIO; cuenta++ ) { vertical2.add( Box.createRigidArea( new Dimension( 12, 8 ) ) ); vertical2.add( new JButton( "Boton " + cuenta ) ); } // fin de for // crea pegamento vertical y agrega botones al panel JPanel panel = new JPanel(); panel.setLayout( new BoxLayout( panel, BoxLayout.Y_AXIS ) ); for ( int cuenta = 0; cuenta < TAMANIO; cuenta++ ) { panel.add( Box.createGlue() ); panel.add( new JButton( "Boton " + cuenta ) ); } // fin de for

Figura 22.16 | Administrador de esquemas BoxLayout. (Parte 1 de 2).

www.elsolucionario.net

909

910

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74

Capítulo 22

Componentes de la GUI: parte 2

// crea un objeto JTabbedPane JTabbedPane fichas = new JTabbedPane( JTabbedPane.TOP, JTabbedPane.SCROLL_TAB_LAYOUT ); // coloca cada contenedor en el panel con fichas fichas.addTab( "Cuadro horizontal", horizontal1 ); fichas.addTab( "Cuadro vertical con montantes", vertical1 ); fichas.addTab( "Cuadro horizontal con pegamento", horizontal2 ); fichas.addTab( "Cuadro vertical con areas rigidas", vertical2 ); fichas.addTab( "Cuadro vertical con pegamento", panel ); add( fichas ); // coloca panel con fichas en el marco } // fin del constructor de MarcoBoxLayout } // fin de la clase MarcoBoxLayout

Figura 22.16 | Administrador de esquemas BoxLayout. (Parte 2 de 2). rencias a contenedores Box con un esquema BoxLayout vertical, en el que los componentes de la GUI se ordenan de arriba hacia abajo. La instrucción for de las líneas 27 y 28 agrega tres objetos JButton a horizontal1. La instrucción for de las líneas 31 a 35 agrega tres objetos JButton a vertical1. Antes de agregar cada botón, en la línea 33 se agrega un montante vertical al contenedor, mediante el método estático createVerticalStrut de Box. Un montante vertical es un componente de la GUI invisible, el cual tiene una altura fija en píxeles y se utiliza para garantizar una cantidad fija de espacio entre los componentes de la GUI. El argumento int para el método createVerticalStrut determina la altura del montante, en píxeles. Cuando se cambia el tamaño del contenedor, la distancia entre los componentes de la GUI que están separados por montantes no cambia. La clase Box también declara el método createHorizontalStrut para esquemas BoxLayout horizontales. La instrucción for de las líneas 38 a 42 agrega tres objetos JButton a horizontal2. Antes de agregar cada botón, en la línea 40 se agrega pegamento horizontal al contenedor, mediante el método estático createHorizontalGlue de Box. El pegamento horizontal es un componente de la GUI invisible que puede usarse entre los componentes de la GUI de tamaño fijo para ocupar espacio adicional. Generalmente, el espacio adicional aparece a la derecha del último componente de la GUI horizontal, o debajo del último componente de la GUI vertical, en un esquema BoxLayout. El pegamento permite que se agregue espacio adicional entre los componentes de la GUI. Cuando se cambia el tamaño del contenedor, los componentes separados por pegamento conservan el mismo tamaño, pero el pegamento se estira o se contrae para ocupar el espacio entre los demás componentes. La clase Box también declara el método createVerticalGlue para esquemas BoxLayout verticales. La instrucción for de las líneas 45 a 49 agrega tres objetos JButton a vertical2. Antes de agregar cada botón, en la línea 47 se agrega un área rígida al contenedor, mediante el método static createRigidArea de Box. Un área rígida es un componente de la GUI invisible, el cual siempre tiene una anchura y altura fijas, en píxeles. El argumento para el método createRigidArea es un objeto Dimension que especifica la anchura y la altura del área rígida. En las líneas 52 y 53 se crea un objeto JPanel y se establece su esquema en BoxLayout de la manera convencional, utilizando el método setLayout de Container. El constructor de BoxLayout recibe una referencia al contenedor para el que está controlando el esquema, y una constante que indica si el esquema es horizontal (BoxLayout.X_AXIS) o vertical (BoxLayout.Y_AXIS). La instrucción for de las líneas 55 a 59 agrega tres objetos JButton al objeto panel. Antes de agregar cada botón, en la línea 57 se agrega un componente pegamento al contenedor, mediante el método static createGlue de Box. Este componente se expande o se contrae con base en el tamaño del objeto Box. En las líneas 62 y 63 se crea un objeto JTabbedPane para mostrar los cinco contenedores en este programa. El argumento JTabbedPane.TOP que se envía al constructor indica que las fichas deben aparecer en la parte superior del objeto JTabbedPane. El argumento JTabbedPane.SCROLL_TAB_LAYOUT especifica que las fichas deben desplazarse si hay demasiadas como para que puedan ajustarse en una sola línea.

www.elsolucionario.net

22.9

1 2 3 4 5 6 7 8 9 10 11 12 13 14

Administradores de esquemas: BoxLayout y GridBagLayout

911

// Fig. 22.17: DemoBoxLayout.java // Demostración de BoxLayout. import javax.swing.JFrame; public class DemoBoxLayout { public static void main( String args[] ) { MarcoBoxLayout marcoBoxLayout = new MarcoBoxLayout(); marcoBoxLayout.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); marcoBoxLayout.setSize( 400, 220 ); // establece el tamaño del marco marcoBoxLayout.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase DemoBoxLayout

Flechas para pasar de una ficha a otra

Figura 22.17 | Clase de prueba para MarcoBoxLayout.java . Los contenedores Box y el objeto JPanel se adjuntan al objeto JTabbedPane en las líneas 66 a 70. Ahora pruebe a ejecutar la aplicación. Cuando aparezca la ventana, cambie su tamaño para ver cómo afectan los componentes pegamento, montante y área rígida en la distribución de cada ficha.

www.elsolucionario.net

912

Capítulo 22

Componentes de la GUI: parte 2

Administrador de esquemas GridBagLayout Uno de los administradores de esquemas predefinidos más complejos y poderosos es GridBagLayout (en el paquete java.awt). Este esquema es similar a GridLayout, ya que también ordena los componentes en una cuadrícula. Sin embargo, el esquema GridBagLayout es más flexible. Los componentes pueden variar en tamaño (es decir, pueden ocupar varias filas y columnas) y pueden agregarse en cualquier orden. El primer paso para utilizar GridBagLayout es determinar la apariencia de la GUI. Para este paso sólo se necesita un pedazo de papel. Dibuje la GUI y después dibuje una cuadrícula sobre la GUI, dividiendo los componentes en filas y columnas. Los números iniciales de fila y columna deben ser 0, de manera que el esquema GridBagLayout pueda usar los números de fila y columna para colocar apropiadamente los componentes en la cuadrícula. En la figura 22.18 se demuestra cómo dibujar las líneas para las filas y columnas sobre una GUI. Columna 1

0

2

0 1 Fila 2 3

Figura 22.18 | Diseño de una GUI que utilizará a GridBagLayout. Un objeto GridBagConstraints describe cómo colocar un componente en un esquema GridBagLayout. Varios campos de GridBagConstraints se sintetizan en la figura 22.19. El campo anchor de GridBagConstraints especifica la posición relativa del componente en un área que no rellena. A la variable anchor se le asigna una de las siguientes constantes de GridBagConstraints: NORTH, NORTHEAST, EAST, SOUTHEAST, SOUTH, SOUTHWEST, WEST, NORTHWEST o CENTER. El valor predeterminado es CENTER.

Campo de GridBagConstraints anchor

Descripción Especifica la posición relativa (NORTH, NORTHEAST, EAST, SOUTHEAST, SOUTH, SOUTHWEST, del componente en un área que no rellena.

WEST, NORTHWEST, CENTER) fill

Ajusta el tamaño del componente en la dirección especificada (NONE, HORIZONTAL, VERTICAL, BOTH) cuando el área en pantalla es más grande que el componente.

gridx

La columna en la que se colocará el componente.

gridy

La fila en la que se colocará el componente.

gridwidth

El número de columnas que ocupa el componente.

gridheight

El número de filas que ocupa el componente.

weightx

La porción de espacio adicional que se asignará horizontalmente. La ranura de la cuadrícula puede hacerse más ancha cuando haya espacio adicional disponible.

weighty

La porción de espacio adicional que se asignará verticalmente. La ranura de la cuadrícula puede hacerse más alta cuando haya espacio adicional disponible.

Figura 22.19 | Campos de GridBagConstraints.

www.elsolucionario.net

22.9

Administradores de esquemas: BoxLayout y GridBagLayout

913

El campo fill de GridBagConstraints define la forma en que crece el componente, si el área en la que puede mostrarse es mayor que el componente. A la variable fill se le asigna una de las siguientes constantes de GridBagConstraints: NONE, VERTICAL, HORIZONTAL o BOTH. El valor predeterminado es NONE, el cual indica que el componente no crecerá en ninguna dirección. VERTICAL indica que crecerá en forma vertical. HORIZONTAL indica que crecerá en forma horizontal. BOTH indica que crecerá en ambas direcciones. Las variables gridx y gridy especifican la posición que tendrá la esquina superior izquierda del componente en la cuadrícula. La variable gridx corresponde a la columna, y la variable gridy corresponde a la fila. En la figura 22.18, el objeto JComboBox (que muestra la cadena “Hierro”) tiene un valor de gridx de 1 y un valor de gridy de 2. La variable gridwidth especifica el número de columnas que ocupa un componente. El objeto JComboBox ocupa dos columnas. La variable gridheight especifica el número de filas que ocupa un componente. El objeto JTextArea del lado izquierdo de la figura 22.18 ocupa tres filas. La variable weightx especifica cómo distribuir el espacio horizontal adicional en las ranuras de cuadrícula de un esquema GridBagLayout, cuando se ajusta el tamaño del contenedor. Un valor de cero indica que la ranura de la cuadrícula no crecerá horizontalmente por sí sola. No obstante, si el componente abarca una columna que contenga un componente con un valor de weightx distinto de cero, el componente con valor de weightx de cero crecerá horizontalmente en la misma proporción que el (los) otro(s) componente(s) en la misma columna. Esto se debe a que cada componente debe mantenerse en las mismas fila y columna en las que se colocó originalmente. La variable weighty especifica cómo distribuir el espacio vertical adicional en las ranuras de cuadrícula de un esquema GridBagLayout, cuando se ajusta el tamaño del contenedor. Un valor de cero indica que la ranura de la cuadrícula no crecerá verticalmente por sí sola. No obstante, si el componente abarca una columna que contenga un componente con un valor de weighty distinto de cero, el componente con valor de weighty de cero crecerá verticalmente en la misma proporción que el (los) otro(s) componente(s) en la misma columna. En la figura 22.18, los efectos de weighty y weightx no podrán verse fácilmente sino hasta que se ajuste el tamaño del contenedor y haya más espacio adicional disponible. Los componentes con valores mayores para weightx y weighty ocupan más espacio adicional que los componentes con valores menores. Los componentes deben recibir valores positivos distintos de cero para weightx y weighty; de lo contrario, se “amontonarán” al centro del contenedor. En la figura 22.20 se muestra la GUI de la figura 22.18, con un valor de cero para weightx y weighty.

Figura 22.20 | El GridBagLayout con los valores establecidos en 0. La aplicación de las figuras 22.21 y 22.2 utiliza el administrador de esquemas GridBagLayout para ordenar los componentes que se encuentran en la GUI de la figura 22.18. Este programa no hace nada más que demostrar cómo utilizar el esquema GridBagLayout. La GUI consiste de tres objetos JButton, dos objetos JTextArea, un objeto JComboBox y un objeto JTextField. El administrador de esquemas para el panel de contenido es GridBagLayout. En las líneas 21 y 22 se crea

www.elsolucionario.net

914

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

Capítulo 22

Componentes de la GUI: parte 2

// Fig. 22.21: MarcoGridBag.java // Demostración de GridBagLayout. import java.awt.GridBagLayout; import java.awt.GridBagConstraints; import java.awt.Component; import javax.swing.JFrame; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.JButton; import javax.swing.JComboBox; public class MarcoGridBag extends JFrame { private GridBagLayout esquema; // esquema de este marco private GridBagConstraints restricciones; // restricciones de este esquema // establece la GUI public MarcoGridBag() { super( "GridBagLayout" ); esquema = new GridBagLayout(); setLayout( esquema ); // establece el esquema del marco restricciones = new GridBagConstraints(); // instancia las restricciones // crea los componentes de la GUI JTextArea areaTexto1 = new JTextArea( "AreaTexto1", 5, 10 ); JTextArea areaTexto2 = new JTextArea( "AreaTexto2", 2, 2 ); String nombres[] = { "Hierro", "Acero", "Bronce" }; JComboBox cuadroComb = new JComboBox( nombres ); JTextField campoTexto = new JTextField( JButton boton1 = new JButton( "Boton 1" JButton boton2 = new JButton( "Boton 2" JButton boton3 = new JButton( "Boton 3"

"CampoTexto" ); ); ); );

// weightx y weighty para areaTexto1 son 0: el valor predeterminado // anchor para todos los componentes es CENTER: el valor predeterminado restricciones.fill = GridBagConstraints.BOTH; agregarComponente( areaTexto1, 0, 0, 1, 3 ); // weightx y weighty para boton1 son 0: el valor predeterminado restricciones.fill = GridBagConstraints.HORIZONTAL; agregarComponente( boton1, 0, 1, 2, 1 ); // weightx y weighty para cuadroComb son 0: el valor predeterminado // fill es HORIZONTAL agregarComponente( cuadroComb, 2, 1, 2, 1 ); // boton2 restricciones.weightx = 1000; // puede hacerse más ancho restricciones.weighty = 1; // puede hacerse más alto restricciones.fill = GridBagConstraints.BOTH; agregarComponente( boton2, 1, 1, 1, 1 ); // fill es BOTH para boton3 restricciones.weightx = 0; restricciones.weighty = 0; agregarComponente( boton3, 1, 2, 1, 1 );

Figura 22.21 | Administrador de esquemas GridBagLayout. (Parte 1 de 2).

www.elsolucionario.net

22.9

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79

Administradores de esquemas: BoxLayout y GridBagLayout

915

// weightx y weighty para campoTexto son 0, fill es BOTH agregarComponente( campoTexto, 3, 0, 2, 1 ); // weightx y weighty para areaTexto2 son 0, fill es BOTH agregarComponente( areaTexto2, 3, 2, 1, 1 ); } // fin del constructor de MarcoGridBag // método para establecer restricciones private void agregarComponente( Component componente, int fila, int columna, int anchura, int altura ) { restricciones.gridx = columna; // establece gridx restricciones.gridy = fila; // establece gridy restricciones.gridwidth = anchura; // establece gridwidth restricciones.gridheight = altura; // establece gridheight esquema.setConstraints( componente, restricciones ); // establece restricciones add( componente ); // agrega el componente } // fin del método agregarComponente } // fin de la clase MarcoGridBag

Figura 22.21 | Administrador de esquemas GridBagLayout. (Parte 2 de 2). el objeto GridBagLayout y se establece el administrador de esquemas para el panel de contenido en esquema. En la línea 23 se crea el objeto GridBagConstraints utilizado para determinar la ubicación y el tamaño de cada uno de los componentes en la cuadrícula. En las líneas 26 a 35 se crea cada uno de los componentes de la GUI que se agregarán al panel de contenido. En las líneas 39 a 40 se configura el objeto JTextArea areaTexto1 y se agrega al panel de contenido. Los valores para weightx y weighty no se especifican en restricciones, por lo que cada una de ellas tiene el valor de cero, de manera predeterminada. Por lo tanto, el objeto JTextArea no se cambiará de tamaño a sí mismo, incluso si hay espacio disponible. Sin embargo, el objeto JTextArea abarca varias filas por lo que el tamaño vertical está sujeto a los valores de weighty de los objetos JButton boton2 y boton3. Cuando se cambie el tamaño de uno de los objetos boton2 o boton3 en forma vertical, con base en su valor de weighty, también se cambiará el tamaño del objeto JTextArea. En la línea 39 se establece la variable fill de restricciones en GridBagConstraints.BOTH, lo cual hará que el objeto JTextArea siempre llene toda su área asignada en la cuadrícula. No se especifica un valor para anchor en restricciones, por lo que se utiliza el valor predeterminado de CENTER. Como no utilizamos la variable anchor en esta aplicación, todos los componentes utilizarán su valor predeterminado. En la línea 40 se hace una llamada a nuestro método utilitario agregarComponente (declarado en las líneas 69 a 78). El objeto JTextArea, la fila, la columna, el número de columnas y el número de filas a abarcar se pasan como argumentos. El objeto JButton boton1 es el siguiente componente que se agrega (líneas 43 y 44). De manera predeterminada, los valores de weightx y weighty siguen siendo cero. La variable fill se establece en HORIZONTAL; el componente siempre ocupará toda su área en dirección horizontal. La dirección vertical no se ocupará toda. El valor de weighty es cero, por lo que el botón sólo será más alto si otro componente en la misma fila tiene un valor de weighty distinto de cero. El objeto JButton boton1 se encuentra en la fila 0, columna 1. Se ocupan una fila y dos columnas. El objeto JComboBox cuadroCombinado es el siguiente componente que se agrega (línea 48). De manera predeterminada, los valores de weightx y weighty son cero y la variable fill se establece en HORIZONTAL. El botón JComboBox crecerá solamente en dirección horizontal. Observe que las variables weightx, weighty y fill retienen los valores establecidos en restricciones hasta que se modifiquen. El botón JComboBox se coloca en la fila 2, columna 1. Se ocupan una fila y dos columnas. El objeto JButton boton2 es el siguiente componente que se agrega (líneas 51 a 54). Obtiene un valor de weightx de 1000 y un valor de weighty de 1. El área ocupada por el botón es capaz de crecer en las direcciones vertical y horizontal. La variable fill se establece en BOTH, lo cual especifica que el botón siempre ocupará toda el área. Cuando se ajuste el tamaño de la ventana, boton2 crecerá. El botón se coloca en la fila 1, columna 1. Se ocupan una fila y una columna.

www.elsolucionario.net

916

1 2 3 4 5 6 7 8 9 10 11 12 13 14

Capítulo 22

Componentes de la GUI: parte 2

// Fig. 22.22: DemoGridBag.java // Demostración de MarcoBagLayout. import javax.swing.JFrame; public class DemoGridBag { public static void main( String args[] ) { MarcoGridBag marcoGridBag = new MarcoGridBag(); marcoGridBag.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); marcoGridBag.setSize( 300, 150 ); // establece el tamaño del marco marcoGridBag.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase DemoGridBag

Figura 22.22 | Clase de prueba para MarcoGridBag. El objeto JButton boton3 se agrega a continuación (líneas 57 a 59). Los valores de weightx y weighty son cero, y la variable fill se establece en BOTH. El objeto JButton boton3 crecerá si se ajusta el tamaño de la ventana; se verá afectado por los valores de weightx y weighty de boton2. Observe que el valor de weightx para boton2 es mucho mayor que el de boton3. Cuando ocurra el cambio de tamaño, boton2 ocupará un mayor porcentaje del nuevo espacio. El botón se coloca en la fila 1, columna 2. Se ocupan una fila y una columna. Tanto el objeto JTextField campoTexto (línea 62) como el objeto JTextArea areaTexto2 (línea 65) tienen un valor de weightx de 0 y un valor de weighty de 0. El valor de fill es BOTH. El objeto JTextField se coloca en la fila 3, columna 0, y el objeto JTextArea se coloca en la fila 3, columna 2. El objeto JTextField ocupa una fila y dos columnas. El objeto JTextArea ocupa una fila y una columna. Los parámetros del método agregarComponente son una referencia Component llamada componente, y los enteros fila, columna, anchura y altura. En las líneas 72 y 73 se establecen las variables de GridBagConstraints llamadas gridx y gridy. A la variable gridx se le asigna la columna en la que se colocará el objeto

www.elsolucionario.net

22.9

Administradores de esquemas: BoxLayout y GridBagLayout

917

Component, y a la variable gridy se le asigna la fila en la que se colocará el objeto Component. En las líneas 74 y 75 se establecen las variables de GridBagConstraints llamadas gridwidth y gridheight. La variable gridwidth especifica el número de columnas que abarcará el objeto Component en la cuadrícula, y la variable gridheight especifica el número de filas que abarcará el objeto Component en la cuadrícula. En la línea 76 se establecen los valores del objeto GridBagConstraints para un componente en el esquema GridBagLayout. El método setConstraints de la clase GridBagLayout recibe un argumento Component y un argumento GridBagConstraints. En la línea 77 se agrega el componente al objeto JFrame.

Cuando ejecute esta aplicación, pruebe a cambiar el tamaño de la ventana para ver cómo las restricciones para cada componente de la GUI afectan su posición y tamaño en la ventana.

Las constantes RELATIVE y REMAINDER de GridBagConstraints

Hay una variación de GridBagLayout que no utiliza a las variables gridx y gridy. En vez de estas variables, se utilizan las constantes RELATIVE y REMAINDER de GridBagConstraints. RELATIVE especifica que el penúltimo componente de cierta fila deberá colocarse a la derecha del componente anterior en esa fila. REMAINDER especifica que un componente es el último en una fila. Cualquier componente que no sea el penúltimo o el último en una fila, deberá especificar valores para las variables gridwidth y gridheight de GridbagConstraints. La aplicación de las figuras 22.23 y 22.24 ordena los componentes de un esquema Grid-BagLayout, utilizando estas constantes. En las líneas 21 y 22 se crea un objeto GridBagLayout y se utiliza para establecer el administrador de esquemas del objeto JFrame. Los componentes que se colocan en el esquema GridBagLayout se crean en las líneas 27 a 38; son un objeto JComboBox, un objeto JTextField, un objeto JList y cinco objetos JButton. El objeto JTextField se agrega primero (líneas 41 a 45). Los valores de weightx y weighty se establecen en 1. La variable fill se establece en BOTH. En la línea 44 se especifica que el objeto JTextField es el último componente de la línea. El objeto JTextField se agrega al panel de contenido mediante una llamada a nuestro método utilitario agregarComponente (declarado en las líneas 79 a 83). El método agregarComponente recibe un argumento Component y utiliza el método setConstraints de GridBagLayout para establecer las restricciones de este objeto Component. El método add adjunta el componente al panel de contenido.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

// Fig. 22.23: MarcoGridBag2.java // Demostración de las constantes de GridBagLayout. import java.awt.GridBagLayout; import java.awt.GridBagConstraints; import java.awt.Component; import javax.swing.JFrame; import javax.swing.JComboBox; import javax.swing.JTextField; import javax.swing.JList; import javax.swing.JButton; public class MarcoGridBag2 extends JFrame { private GridBagLayout esquema; // esquema de este marco private GridBagConstraints restricciones; // restricciones de este esquema // establece la GUI public MarcoGridBag2() { super( "GridBagLayout" ); esquema = new GridBagLayout(); setLayout( esquema ); // establece el esquema del marco restricciones = new GridBagConstraints(); // instancia las restricciones // crea los componentes de la GUI String metales[] = { "Cobre", "Aluminio", "Plata" };

Figura 22.23 | Las constantes RELATIVE y REMAINDER de GridBagConstraints. (Parte 1 de 2).

www.elsolucionario.net

918

27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84

Capítulo 22

Componentes de la GUI: parte 2

JComboBox cuadroComb = new JComboBox( metales ); JTextField campoTexto = new JTextField( "CampoTexto" ); String fuentes[] = { "Serif", "Monospaced" }; JList lista = new JList( fuentes ); String nombres[] = { "cero", "uno", "dos", "tres", "cuatro" }; JButton botones[] = new JButton[ nombres.length ]; for ( int cuenta = 0; cuenta < botones.length; cuenta++ ) botones[ cuenta ] = new JButton( nombres[ cuenta ] ); // define las restricciones para el componente de la GUI campoTexto restricciones.weightx = 1; restricciones.weighty = 1; restricciones.fill = GridBagConstraints.BOTH; restricciones.gridwidth = GridBagConstraints.REMAINDER; agregarComponente( campoTexto ); // botones[0] -- weightx y weighty son 1: fill es BOTH restricciones.gridwidth = 1; agregarComponente( botones[ 0 ] ); // botones[1] -- weightx y weighty son 1: fill es BOTH restricciones.gridwidth = GridBagConstraints.RELATIVE; agregarComponente( botones[ 1 ] ); // botones[2] -- weightx y weighty son 1: fill es BOTH restricciones.gridwidth = GridBagConstraints.REMAINDER; agregarComponente( botones[ 2 ] ); // cuadroComb -- weightx es 1: fill es BOTH restricciones.weighty = 0; restricciones.gridwidth = GridBagConstraints.REMAINDER; agregarComponente( cuadroComb ); // botones[3] -- weightx es 1: fill es BOTH restricciones.weighty = 1; restricciones.gridwidth = GridBagConstraints.REMAINDER; agregarComponente( botones[ 3 ] ); // botones[4] -- weightx y weighty son 1: fill es BOTH restricciones.gridwidth = GridBagConstraints.RELATIVE; agregarComponente( botones[ 4 ] ); // lista -- weightx y weighty son 1: fill es BOTH restricciones.gridwidth = GridBagConstraints.REMAINDER; agregarComponente( lista ); } // fin del constructor de MarcoGridBag2 // agrega un componente al contenedor private void agregarComponente( Component componente ) { esquema.setConstraints( componente, restricciones ); add( componente ); // agrega el componente } // fin del método agregarComponente } // fin de la clase MarcoGridBag2

Figura 22.23 | Las constantes RELATIVE y REMAINDER de GridBagConstraints. (Parte 2 de 2).

www.elsolucionario.net

22.9

Administradores de esquemas: BoxLayout y GridBagLayout

919

El objeto JButton botones[ 0 ] (líneas 48 y 49) tiene valores de weightx y weighty de 1. La variable fill tiene el valor de BOTH. Como botones[ 0 ] no es uno de los últimos componentes de la fila, recibe un valor de gridwidth de 1, de manera que ocupe solamente una columna. El objeto JButton se agrega al panel de contenido mediante una llamada al método utilitario agregarComponente. El objeto JButton botones[ 1 ] (líneas 52 y 53) tiene valores de weightx y weighty de 1. La variable fill tiene el valor de BOTH. En la línea 52 se especifica que el objeto JButton se va a colocar de manera relativa al componente anterior. El objeto JButton se agrega al objeto JFrame mediante una llamada a agregarComponente. El objeto JButton botones[ 2 ] (líneas 56 y 57) tiene valores de weightx y weighty de 1. La variable fill tiene el valor de BOTH. Este objeto JButton es el último componente de la línea, por lo que se utiliza REMAINDER. El objeto JButton se agrega al panel de contenido mediante una llamada a agregarComponente. El objeto JComboBox (líneas 60 a 62) tiene un valor de weightx de 1 y un valor de weighty de 0. El objeto JComboBox no crecerá en dirección vertical. Este objeto JComboBox es el único componente de la línea, por lo que se utiliza REMAINDER. El objeto JComboBox se agrega al panel de contenido mediante una llamada a agregarComponente. El objeto JButton botones[ 3 ] (líneas 65 a 67) tiene valores de weightx y weighty de 1. La variable fill tiene el valor de BOTH. Este objeto JButton es el único componente de la línea, por lo que se utiliza REMAINDER. El objeto JButton se agrega al panel de contenido mediante una llamada a agregarComponente.

1 2 3

// Fig. 22.24: DemoGridBag2.java // Demostración de las constantes de GridBagLayout. import javax.swing.JFrame;

4 5 6 7 8 9 10 11 12 13 14

public class DemoGridBag2 { public static void main( String args[] ) { MarcoGridBag2 marcoGridBag2 = new MarcoGridBag2(); marcoGridBag2.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); marcoGridBag2.setSize( 300, 200 ); // establece el tamaño del marco marcoGridBag2.setVisible( true ); // muestra el marco } // fin de main } // fin de la clase DemoGridBag2

Figura 22.24 | Clase de prueba para DemoGridBag2.

El objeto JButton botones[ 4 ] (líneas 70 y 71) tiene valores de weightx y weighty de 1. La variable fill tiene el valor de BOTH. Este objeto JButton es el penúltimo componente de la línea, por lo que se utiliza RELATIVE. El objeto JButton se agrega al panel de contenido mediante una llamada a agregarComponente. El objeto JList (líneas 74 y 75) tiene valores de weightx y weighty de 1. La variable fill tiene el valor de BOTH. El objeto JList se agrega al panel de contenido mediante una llamada a agregarComponente.

www.elsolucionario.net

920

Capítulo 22

Componentes de la GUI: parte 2

22.10 Conclusión Este capítulo completa nuestra introducción a la GUI. Aquí aprendió acerca de temas más avanzados sobre la GUI, como los menús, controles deslizables, menús contextuales y la interfaz de múltiples documentos. Todos estos componentes se pueden agregar a las aplicaciones existentes, para que sean más fáciles de usar y de entender. En el siguiente capítulo aprenderá acerca del subprocesamiento múltiple, una poderosa herramienta que permite a las aplicaciones utilizar subprocesos para realizar varias tareas a la vez.

Resumen Sección 22.2 JSlider • Los objetos JSlider permiten al usuario seleccionar de entre un rango de valores enteros. Los objetos JSlider pueden mostrar marcas más distanciadas, marcas menos distanciadas y etiquetas para las marcas. También soportan el ajuste a la marca, en el que al colocar el indicador entre dos marcas, éste se ajusta a la marca más cercana. • Si un objeto JSlider tiene el foco, la tecla de flecha izquierda y la tecla de flecha derecha hacen que el indicador del objeto JSlider se decremente o se incremente en 1. La tecla de flecha hacia abajo y la tecla de flecha hacia arriba también hacen que el indicador del objeto JSlider se decremente o incremente en 1, respectivamente. Las teclas Av Pág (avance de página) y Re Pág (retroceso de página) hacen que el indicador del objeto JSlider se decremente o incremente en incrementos de bloque de una décima parte del rango de valores, respectivamente. La tecla Inicio desplaza el indicador hacia el valor mínimo del objeto JSlider, y la tecla Fin desplaza el indicador hacia el valor máximo del objeto JSlider. • El método setMajorTickSpacing de la clase JSlider establece el espaciado para las marcas en un objeto JSlider. El método setPaintTicks con un argumento true indica que las marcas deben mostrarse. • Los objetos JSlider generan eventos ChangeEvent en respuesta a las interacciones del usuario. Un objeto ChangeListener declara el método stateChanged, el cual puede responder a los eventos ChangeEvent.

Sección 22.3 Ventanas: observaciones adicionales • Todas las ventanas generan eventos de ventana cuando el usuario las manipula. La interfaz WindowListener proporciona siete métodos para manejo de eventos de ventana: windowActivated, windowClosed, windowClosing, windowDeactivated, windowDeiconified, windowIconified y windowOpened. • Los menús son una parte integral de las GUIs, ya que permiten al usuario realizar acciones sin atestar innecesariamente una interfaz gráfica de usuario con componentes de la GUI adicionales. En las GUIs de Swing, los menús sólo se pueden adjuntar a objetos de clases que contengan el método setJMenuBar (por ejemplo, JFrame y JApplet).

Sección 22.4 Uso de menús con marcos • Las clases que se utilizan para declarar menús son JMenuBar, JMenuItem, JMenu, JCheckBoxMenuItem y JRadioButtonMenuItem. • Un objeto JMenuBar es un contenedor de menús. Un objeto JMenuItem es un componente de GUI dentro de un menú que, al ser seleccionado, hace que se realice una acción. Un objeto JMenu contiene elementos de menú y puede agregarse a un objeto JMenuBar o a otros objetos JMenu como submenús. • Cuando hacemos clic en un menú, éste se expande para mostrar su lista de elementos. El método addSeparator de JMenu agrega una línea separadora a un menú. • Al seleccionar un objeto JCheckBoxMenuItem, aparece una marca de verificación a la izquierda del elemento de menú. Cuando se selecciona de nuevo el objeto JCheckBoxMenuItem, la marca se elimina. • Cuando se mantienen varios objetos JRadioButtonMenu como parte de un objeto ButtonGroup, sólo un objeto en el grupo puede seleccionarse en un momento dado. Cuando se selecciona un elemento, aparece un círculo relleno a su izquierda. Cuando se selecciona otro objeto JRadioButtonMenu, se elimina el círculo relleno a la izquierda del elemento antes seleccionado. • El método setMnemonic de AbstractButton especifica el nemónico para un objeto AbstractButton. Los caracteres nemónicos se muestran generalmente subrayados. • Un cuadro de diálogo modal no permite el acceso a ninguna otra ventana en la aplicación, a menos que se cierre. Los cuadros de diálogo que se muestran con la clase JOptionPane son modales. Puede utilizar la clase JDialog para crear sus propios cuadros de diálogo modales o no modales.

www.elsolucionario.net

Terminología

921

Sección 22.5 JPopupMenu • Los menús contextuales sensibles al contexto se crean mediante la clase JPopupMenu. En la mayoría de los sistemas, el evento de desencadenamiento del menú contextual ocurre cuando el usuario oprime y suelta el botón derecho del ratón. El método isPopupTrigger de MouseEvent devuelve true si ocurrió el evento de desencadenamiento del menú contextual. • El método show de JPopupMenu muestra un objeto JPopupMenu. El primer argumento especifica el componente de origen, el cual ayuda a determinar en dónde aparecerá el objeto JPopupMenu. Los últimos dos argumentos son las coordenadas a partir de la esquina superior izquierda del componente de origen, en donde aparece el objeto JPopupMenu.

Sección 22.6 Apariencia visual adaptable • La clase UIManager contiene la clase anidada LookAndFeelInfo que mantiene la información acerca de una apariencia visual. • El método static getInstalledLookAndFeels de UIManager obtiene un arreglo de objetos UIManager.LookAndFeelInfo que describe cada una de las apariencias visuales disponibles. • El método static setLookAndFeel de UIManager cambia la apariencia visual. El método static updateComponentTreeUI de la clase SwingUtilities cambia la apariencia visual de todos los componentes adjuntos a su argumento Component a la nueva apariencia visual.

Sección 22.7 JDesktopPane y JInternalFrame • Muchas de las aplicaciones de la actualidad utilizan una interfaz de múltiples documentos (MDI) para administrar varios documentos abiertos, los cuales se procesan en paralelo. Las clases JDesktopPane y JInternalFrame de Swing proporcionan soporte para crear interfaces de múltiples documentos.

Sección 22.8 JTabbedPane • Un objeto JTabbedPane ordena los componentes de la GUI en capas, de las cuales sólo una puede verse en un momento dado. Los usuarios acceden a cada capa a través de una ficha; muy parecido a las carpetas en un archivero. Cuando el usuario hace clic en una ficha, se muestra la capa apropiada.

Sección 22.9 Administradores de esquemas: BoxLayout y GridBagLayout •

es un administrador de esquemas que permite ordenar los componentes de la GUI de izquierda a derecha, o de arriba hacia abajo, en un contenedor. • La clase Box declara un contenedor con BoxLayout como su administrador de esquemas predeterminado, y proporciona métodos estáticos para crear un objeto Box con un esquema BoxLayout horizontal o vertical. • GridBagLayout es un administrador de esquemas similar a GridLayout. A diferencia de GridLayout, el tamaño de cada componente puede variar y pueden agregarse componentes en cualquier orden. • Un objeto GridBagConstraints describe cómo colocar un componente en un esquema GridBagLayout. El método setConstraints de la clase GridBagLayout recibe un argumento Component y un argumento GridBagConstraints, y establece las restricciones del objeto Component. BoxLayout

Terminología add, método de la clase JMenuBar addWindowListener, método de la clase Window ajuste a la marca para JSlider anchor, campo de la clase GridBagConstraints

apariencia visual adaptable (PLAF) apariencia visual metálica área rígida barra de menús barra de título borde BOTH, constante de la clase GridBagConstraints Box, clase BoxLayout, clase CENTER, constante de la clase GridBagConstraints ChangeEvent, clase

ChangeListener,

interfaz componente de origen createGlue, método de la clase Box createHorizontalBox, método de la clase Box createHorizontalGlue, método de la clase Box createHorizontalStrut, método de la clase Box createRigidArea, método de la clase Box createVerticalBox, método de la clase Box createVerticalGlue, método de la clase Box createVerticalStrut, método de la clase Box cuadro de diálogo modal Dimension, clase dispose, método de la clase Window documento EAST, constante de la clase GridBagConstraints

www.elsolucionario.net

922

Capítulo 22

Componentes de la GUI: parte 2

elemento de menú envoltura de línea evento de desencadenamiento de un menú contextual eventos de ventana getClassName, método de la clase UIManager.LookAndFeelInfo getInstalledLookAndFeels, método de la clase UIManager getPreferredSize, método de la clase Component getSelectedText, método de la clase JTextComponent getValue, método de la clase JSlider GridBagConstraints, clase GridBagLayout, clase gridheight, campo de la clase GridBagConstraints gridwidth, campo de la clase GridBagConstraints gridx, campo de la clase GridBagConstraints gridy, campo de la clase GridBagConstraints HORIZONTAL, constante de GridBagConstraints indicador de JSlider

interfaz de múltiples documentos (MDI) isPopupTrigger, método de la clase MouseEvent isSelected, método de la clase AbstractButton JCheckBoxMenuItem, clase JDesktopPane, clase JDialog, clase JFrame, clase JInternalFrame, clase JMenu, clase JMenuBar, clase JMenuItem, clase JRadioButtonMenuItem, clase JSlider, clase JTabbedPane, clase línea separadora en un menú LookAndFeelInfo, clase anidada de la clase UIManager marcas en JSlider marcas más distanciadas marcas menos distanciadas de JSlider menú menú contextual sensible al contexto montante vertical nemónico NONE, constante de la clase GridBagConstraints NORTH, constante de la clase GridBagConstraints NORTHEAST, constante de la clase GridBagConstraints NORTHWEST, constante de la clase GridBagConstraints opaco pack, método de la clase Window paintComponent, método de la clase JComponent pegamento horizontal

políticas de desplazamiento RELATIVE, constante de la clase GridBagConstraints REMAINDER, constante de la clase GridBagConstraints setConstraints, método de la clase GridBagLayout setDefaultCloseOperation, método de la clase JFrame setInverted, método de la clase JSlider setJMenuBar, método de la clase JFrame setLocation, método de la clase Component setLookAndFeel, método de la clase UIManager setMajorTickSpacing, método de la clase JSlider setMnemonic, método de la clase AbstractButton setOpaque, método de la clase JComponent setPaintTicks, método de la clase JSlider setSelected, método de la clase AbstractButton setVerticalScrollBarPolicy, método de JSlider show, método de la clase JPopupMenu SOUTH, constante de la clase GridBagConstraints SOUTHEAST, constante de la clase GridBagConstraints SOUTHWEST, constante de la clase GridBagConstraints stateChanged, método de la interfaz ChangeListener

submenú SwingUtilities,

clase tecla de acceso rápido transparencia de un objeto JComponent UIManager, clase updateComponentTreeUI, método de la clase SwingUtilities

ventana hija ventana padre ventana padre para un cuadro de diálogo VERTICAL, constante de la clase GridBagConstraints weightx, campo de la clase GridBagConstraints weighty, campo de la clase GridBagConstraints WEST, constante de la clase GridBagConstraints windowActivated, método de la interfaz Window Listener windowClosing, método de la interfaz Window Listener WindowConstants, interfaz windowDeactivated, método de la interfaz Window Listener windowDeiconified, método de la interfaz Window Listener windowIconified, método de la interfaz Window Listener WindowListener, interfaz windowOpened, método de la interfaz WindowListener X_AXIS, constante de la clase Box Y_AXIS, constante de la clase Box

Ejercicios de autoevaluación 22.1

Complete las siguientes oraciones: a) La clase _________________ se utiliza para crear un objeto menú. b) El método _________________ de la clase JMenu coloca una barra separadora en un menú.

www.elsolucionario.net

Ejercicios

923

c) Los eventos de JSlider son manejados por el método _________________ de la interfaz _____________. d) La variable de instancia _________________ de GridBagConstraints se establece en CENTER de manera predeterminada. 22.2

Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué. a) Cuando el programador crea un objeto JFrame, como mínimo debe crearse y agregarse un menú al objeto JFrame. b) La variable fill pertenece a la clase GridBagLayout. c) La acción de dibujar en un componente de la GUI se realiza con respecto a la coordenada (0, 0) de la esquina superior izquierda del componente. d) El esquema predeterminado para un objeto Box es BoxLayout.

22.3

Encuentre el (los) error(es) en cada una de las siguientes instrucciones y explique cómo corregirlo(s). a) JMenubar b; b) miSlider = JSlider( 1000, 222, 100, 450 ); c) gbc.fill = GridBagConstraints.NORTHWEST; // establece fill d) // sobrescribe a paint en un componente de Swing personalizado public void paintcomponent( Graphics g ) { g.drawString( "HOLA", 50, 50 ); } // fin del metodo Saintcomponent

e)

// crea un objeto JFrame y lo muestra JFrame f = new JFrame( "Una ventana" ); f.setVisible( true );

Respuestas a los ejercicios de autoevaluación 22.1

a)

JMenu.

b) addSeparator. c) stateChanged, ChangeListener. d) anchor.

22.2

a) b) c) d)

Falso. Un objeto JFrame no requiere menús. Falso. La variable fill pertenece a la clase GridBagConstraints. Verdadero. Verdadero.

22.3

a) JMenubar debe ser JMenuBar. b) El primer argumento para el constructor debe ser SwingConstants.HORIZONTAL o SwingConstants.VERTICAL, y debe utilizarse la palabra clave new después del operador =. c) La constante debe ser BOTH, HORIZONTAL, VERTICAL o NONE. d) paintcomponent debe ser paintComponent, y el método debe llamar a super.paintComponent( g ) como su primera instrucción. e) El método setSize de JFrame debe ser llamado también, para establecer el tamaño de la ventana.

Ejercicios 22.4

Complete las siguientes oraciones: a) Un objeto JMenuItem que es un JMenu se llama _________________. b) El método _________________ adjunta un objeto JMenuBar a un objeto JFrame. c) La clase contenedora _________________ tiene un esquema BoxLayout predeterminado. d) Un _________________ administra a un conjunto de ventanas hijas declaradas con la clase JInternalFrame.

22.5

Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué. a) Los menús requieren un objeto JMenuBar para poder adjuntarse a un objeto JFrame. b) BoxLayout es el administrador de esquemas predeterminado para un objeto JFrame. c) El método setEditable es un método de JTextComponent. d) La clase JFrame extiende directamente a la clase Container. e) Los objetos JApplet pueden contener menús.

www.elsolucionario.net

924

Capítulo 22

Componentes de la GUI: parte 2

22.6 Encuentre el (los) error(es) en cada una de las siguientes instrucciones. Explique cómo corregir el (los) error(es). a) x.add( new JMenuItem( "Submenu Color" ) ); // crear submenú b) contenedor.setLayout( m = new GridbagLayout() ); 22.7 Escriba un programa que muestre un círculo de tamaño aleatorio, calcule y muestre su área, radio, diámetro y circunferencia. Use las siguientes ecuaciones: diametro = 2 ∞ radio, area = π ∞ radio2, circunferencia = 2 ∞ π ∞ radio. Use la constante Math.PI para pi (π). Todos los dibujos deberán realizarse en una subclase de JPanel, y los resultados de los cálculos deberán mostrarse en un objeto JTextArea de sólo lectura. 22.8 Mejore el programa del ejercicio 22.7 al permitir al usuario alterar el radio con un objeto JSlider. El programa deberá funcionar para todos los radios en el rango de 100 a 200. A medida que cambie el radio, el diámetro, área y circunferencias deberán actualizarse y mostrarse. El radio inicial debe ser 150. Use las ecuaciones del ejercicio 22.7. Todos los dibujos deberán realizarse en una subclase de JPanel, y los resultados de los cálculos deberán mostrarse en un objeto JTextArea de sólo lectura. 22.9 Explore los efectos de variar los valores de weightx y weighty del programa de la figura 22.21. ¿Qué ocurre cuando una ranura tiene valores distintos de cero para weightx y weighty, pero no se le permite rellenar toda el área (es decir, que el valor fill no sea BOTH)? 22.10 Escriba un programa que utilice el método paintComponent para dibujar el valor actual de un objeto JSlider en una subclase de JPanel. Además, proporcione un objeto JTextField en donde pueda introducirse un valor específico. El objeto JTextField deberá mostrar el valor actual del objeto JSlider en todo momento. Debe usarse un objeto JLabel para identificar al objeto JTextField. Deben utilizarse los métodos setValue y getValue de JSlider. [Nota: El método setValue es un método public que no devuelve un valor y toma un argumento entero: el valor de JSlider, que determina la posición del indicador]. 22.11 Modifique el programa de la figura 22.13, agregando un mínimo de dos fichas nuevas. 22.12 Declare a una subclase de JPanel llamada MiSelectorDeColor, que proporcione tres objetos JSlider y tres objetos JTextField. Cada objeto JSlider representa los valores de 0 a 255 para las partes rojo, azul y verde de un color. Use estos valores como argumentos para el constructor de Color, para crear un nuevo objeto Color. Muestre el valor actual de cada objeto JSlider en el objeto JTextField correspondiente. Cuando el usuario cambie el valor del objeto JSlider, el objeto JTextField deberá cambiar de manera acorde. Use su nuevo componente de la GUI como parte de una aplicación que muestre el valor actual de Color, dibujando un rectángulo relleno. 22.13 Modifique la clase MiSelectorDeColor del ejercicio 22.12 para permitir al usuario introducir un valor entero en un objeto JTextField, para establecer el valor rojo, verde o azul. Cuando el usuario oprima Intro en el objeto JTextField, deberá establecerse el objeto JSlider correspondiente al valor apropiado. 22.14 Modifique la aplicación del ejercicio 22.13 para dibujar el color actual como un rectángulo en una instancia de una subclase de JPanel, que proporcione su propio método paintComponent para dibujar el rectángulo, y proporcione métodos establecer para establecer los valores de rojo, verde y azul para el color actual. Cuando se invoque a uno de los métodos establecer, el objeto deberá repintarse automáticamente a sí mismo (mediante repaint). 22.15 Modifique la aplicación del ejercicio 22.14 para permitir al usuario arrastrar el ratón a lo largo del panel de dibujo (una subclase de JPanel), para dibujar una figura con el color actual. Permita al usuario seleccionar la figura a dibujar. 22.16 Modifique el programa del ejercicio 22.15 para proporcionar al usuario la habilidad de terminar la aplicación, haciendo clic en el cuadro para cerrar en la ventana que se muestre en pantalla, y seleccionando Salir de un menú Archivo. Use las técnicas que se muestran en la figura 22.5. 22.17 (Aplicación de dibujo completa) Utilizando las técnicas desarrolladas en este capítulo y en el capítulo 11, cree una aplicación de dibujo completa. El programa debe utilizar los componentes de la GUI de los capítulos 11 y 22 para permitir al usuario seleccionar la figura, color y características de relleno. Cada figura deberá guardarse en un arreglo de objetos MiFigura, en donde MiFigura será superclase en su jerarquía de clases de figuras. Use un objeto JDesktopPane y objetos JInternalFrame para permitir al usuario crear varios dibujos separados, en ventanas hijas separadas. Cree la interfaz de usuario como una ventana hija separada que contenga todos los componentes de la GUI que permitan al usuario determinar las características de la figura a dibujar. El usuario podrá entonces hacer clic en cualquier objeto JInternalFrame para dibujar la figura.

www.elsolucionario.net

23 Subprocesamiento múltiple La definición más general de la belleza… múltiple deidad en la unidad. —Samuel Taylor Coleridge

No bloquees el camino de la investigación. —Charles Sanders Peirce

Una persona con un reloj sabe qué hora es; una persona con dos relojes nunca estará segura.

OBJETIVOS En este capítulo aprenderá a: Q

Conocer qué son los subprocesos y por qué son útiles.

Q

Conocer cómo nos permiten los subprocesos administrar actividades concurrentes.

Q

Conocer el ciclo de vida de un subproceso.

Aprenda a laborar y esperar.

Q

—Henry Wadsworth Longfellow

Conocer acerca de las prioridades y la programación de subprocesos.

Q

Crear y ejecutar objetos Runnable.

Q

Conocer acerca de la sincronización de subprocesos.

Q

Saber qué son las relaciones productor/consumidor y cómo se implementan con el subprocesamiento múltiple.

Q

Habilitar varios subprocesos para actualizar los componentes de GUI de Swing en forma segura para los subprocesos.

Q

Conocer acerca de las interfaces Callable y Future, que podemos usar para ejecutar tareas que devuelven resultados.

—Proverbio

El mundo avanza tan rápido estos días que el hombre que dice que no puede hacer algo, por lo general, se ve interrumpido por alguien que lo está haciendo. —Elbert Hubbard

www.elsolucionario.net

Pla n g e ne r a l

926

Capítulo 23

23.1 23.2 23.3 23.4

23.5

23.6 23.7 23.8 23.9 23.10 23.11

23.12 23.13

Subprocesamiento múltiple

Introducción Estados de los subprocesos: ciclo de vida de un subproceso Prioridades y programación de subprocesos Creación y ejecución de subprocesos 23.4.1 Objetos Runnable y la clase Thread 23.4.2 Administración de subprocesos con el marco de trabajo Executor Sincronización de subprocesos 23.5.1 Cómo compartir datos sin sincronización 23.5.2 Cómo compartir datos con sincronización: hacer las operaciones atómicas Relación productor/consumidor sin sincronización Relación productor/consumidor: ArrayBlockingQueue Relación productor/consumidor con sincronización Relación productor/consumidor: búferes delimitados Relación productor/consumidor: las interfaces Lock y Condition Subprocesamiento múltiple con GUIs 23.11.1 Realización de cálculos en un subproceso trabajador 23.11.2 Procesamiento de resultados inmediatos con SwingWorker Otras clases e interfaces en java.util.concurrent Conclusión

Resumen | Terminología | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

23.1 Introducción Sería bueno si pudiéramos concentrar nuestra atención en realizar una acción a la vez, y realizarla bien, pero por lo general, eso es difícil. El cuerpo humano realiza una gran variedad de operaciones en paralelo; o, como diremos en este capítulo, operaciones concurrentes. Por ejemplo, la respiración, la circulación de la sangre, la digestión, la acción de pensar y caminar pueden ocurrir de manera concurrente. Todos los sentidos (vista, tacto, olfato, gusto y oído) pueden emplearse al mismo tiempo. Las computadoras también pueden realizar operaciones concurrentes. Para las computadoras personales es tarea común compilar un programa, enviar un archivo a una impresora y recibir mensajes de correo electrónico a través de una red, de manera concurrente. Sólo las computadoras que tienen múltiples procesadores pueden realmente ejecutar varias instrucciones en forma concurrente. Los sistemas operativos en las computadoras con un solo procesador crean la ilusión de la ejecución concurrente, al alternar rápidamente entre una tarea y otra, pero en dichas computadoras sólo se puede ejecutar una instrucción a la vez. La mayoría de los lenguajes de programación no permiten a los programadores especificar actividades concurrentes. Por lo general, los lenguajes proporcionan instrucciones de control secuenciales, las cuales permiten a los programadores especificar que sólo debe realizarse una acción a la vez, en donde la ejecución procede a la siguiente acción una vez que la anterior haya terminado. Históricamente, la concurrencia se ha implementado en forma de funciones primitivas del sistema operativo, disponibles sólo para los programadores de sistemas altamente experimentados. El lenguaje de programación Ada, desarrollado por el Departamento de defensa de los Estados Unidos, hizo que las primitivas de concurrencia estuvieran disponibles ampliamente para los contratistas de defensa dedicados a la construcción de sistemas militares de comando y control. Sin embargo, Ada no se ha utilizado de manera general en las universidades o en la industria. Java pone las primitivas de concurrencia a disposición del programador de aplicaciones, a través del lenguaje y de las APIs. El programador especifica que una aplicación contiene subprocesos de ejecución separados, en donde cada subproceso tiene su propia pila de llamadas a métodos y su propio contador, lo cual le permite ejecutarse concurrentemente con otros subprocesos, al mismo tiempo que comparte los recursos a nivel de aplicación (como la memoria) con estos otros subprocesos. Esta capacidad, llamada subprocesamiento múltiple, no está disponible en los lenguajes C y C++ básicos, los cuales influenciaron el diseño de Java.

www.elsolucionario.net

23.2

Estados de los subprocesos: ciclo de vida de un subproceso

927

Tip de rendimiento 23.1 Un problema con las aplicaciones de un solo subproceso es que las actividades que llevan mucho tiempo deben completarse antes de que puedan comenzar otras. En una aplicación con subprocesamiento múltiple, los subprocesos pueden distribuirse entre varios procesadores (si hay disponibles), de manera que se realicen varias tareas en forma concurrente, lo cual permite a la aplicación operar con más eficiencia. El subprocesamiento múltiple también puede incrementar el rendimiento en sistemas con un solo procesador que simulan la concurrencia; cuando un subproceso no puede proceder (debido a que, por ejemplo, está esperando el resultado de una operación de E/S), otro puede usar el procesador.

A diferencia de los lenguajes que no tienen capacidades integradas de subprocesamiento múltiple (como C y C++) y que, por lo tanto, deben realizar llamadas no portables a las primitivas de subprocesamiento múltiple del sistema operativo, Java incluye las primitivas de subprocesamiento múltiple como parte del mismo lenguaje y como parte de sus bibliotecas. Esto facilita la manipulación de los subprocesos de una manera portable entre plataformas. Hablaremos sobre muchas aplicaciones de la programación concurrente. Por ejemplo, cuando se descarga un archivo extenso (como una imagen, un clip de audio o video) a través de Internet, tal vez el usuario no quiera esperar hasta que se descargue todo un clip completo para empezar a reproducirlo. Para resolver este problema podemos poner varios subprocesos a trabajar; uno para descargar el clip y otro para reproducirlo. Estas actividades se llevan a cabo concurrentemente. Para evitar que la reproducción del clip tenga interrupciones, sincronizamos (coordinamos las acciones de) los subprocesos de manera que el subproceso de reproducción no empiece sino hasta que haya una cantidad suficiente del clip en memoria, como para mantener ocupado al subproceso de reproducción. La Máquina Virtual de Java (JVM) utiliza subprocesos también. Además de crear subprocesos para ejecutar un programa, la JVM también puede crear subprocesos para llevar a cabo tareas de mantenimiento, como la recolección de basura. Escribir programas con subprocesamiento múltiple puede ser un proceso complicado. Aunque la mente humana puede realizar funciones de manera concurrente, a las personas se les dificulta saltar de un tren paralelo de pensamiento al otro. Para ver por qué los programas con subprocesamiento múltiple pueden ser difíciles de escribir y comprender, intente realizar el siguiente experimento: abra tres libros en la página 1 y trate de leerlos concurrentemente. Lea unas cuantas palabras del primer libro; después unas cuantas del segundo y por último otras cuantas del tercero. Luego, regrese a leer las siguientes palabras del primer libro, etcétera. Después de este experimento podrá apreciar muchos de los retos que implica el subprocesamiento múltiple: alternar entre los libros, leer brevemente, recordar en dónde se quedó en cada libro, acercar el libro que está leyendo para poder verlo, hacer a un lado los libros que no está leyendo y, entre todo este caos, ¡tratar de comprender el contenido de cada uno! La programación de aplicaciones concurrentes es una tarea difícil y propensa a errores. Si descubre que debe usar la sincronización en un programa, debe seguir ciertos lineamientos simples. Primero, utilice las clases existentes de la API de Java (como la clase ArrayBlockingQueue que veremos en la sección 23.7, Relación productor/consumidor: ArrayBlockingQueue) que administren la sincronización por usted. Las clases en la API de Java están escritas por expertos, han sido probadas y depuradas por completo, operan con eficiencia y le ayudan a evitar trampas y obstáculos comunes. En segundo lugar, si descubre que necesita una funcionalidad más personalizada de la que se proporciona en las APIs de Java, debe usar la palabra clave synchronized y los métodos wait, notify y notifyAll de Object (que veremos en las secciones 23.5 y 23.8). Por último, si requiere herramientas aún más complejas, entonces debe usar las interfaces Lock y Condition que se presentan en la sección 23.10. Sólo los programadores avanzados que estén familiarizados con las trampas y obstáculos comunes de la programación concurrente deben utilizar las interfaces Lock y Condition. En este capítulo explicaremos estos temas por varias razones: proporcionan una base sólida para comprender cómo las aplicaciones concurrentes sincronizan el acceso a la memoria compartida; los conceptos son importantes de comprender, aun si una aplicación no utiliza estas herramientas de manera explícita; y al demostrarle la complejidad involucrada en el uso de estas características de bajo nivel, esperamos dejar en usted la impresión acerca de la importancia del uso de las herramientas de concurrencia preempaquetadas, siempre que sea posible.

23.2 Estados de los subprocesos: ciclo de vida de un subproceso En cualquier momento dado, se dice que un subproceso se encuentra en uno de varios estados de subproceso (los cuales se muestran en el diagrama de estados de UML de la figura 23.1). Varios de los términos en el diagrama se definen en secciones posteriores.

www.elsolucionario.net

928

Capítulo 23

Subprocesamiento múltiple

nuevo

el programa inicia el subproceso

notify notifyAll

wait sleep

el intervalo expira

de

interrupt

ión

adquiere bloqueo

en espera sincronizado

se completa la tarea

en espera

en

sy tra a nc in hr stru em on cc ite iz ión pe ed tic

se completa E/S

ejecutable fy l ti Al no ify t no it wa

E/S

terminado

bloqueado

Figura 23.1 | Diagrama de estado de UML del ciclo de vida de un subproceso. Un nuevo subproceso empieza su ciclo cuando hace la transición al estado nuevo; permanece en este estado hasta que el programa inicia el subproceso, con lo cual se coloca en el estado ejecutable. Se considera que un subproceso en el estado ejecutable está ejecutando su tarea. Algunas veces, un subproceso ejecutable cambia al estado en espera mientras espera a que otro subproceso realice una tarea. Un subproceso en espera regresa al estado ejecutable sólo cuando otro subproceso notifica al subproceso esperando que puede continuar ejecutándose. Un subproceso ejecutable puede entrar al estado en espera sincronizado durante un intervalo específico de tiempo. Regresa al estado ejecutable cuando ese intervalo de tiempo expira, o cuando ocurre el evento que está esperando. Los subprocesos en espera sincronizado y en espera no pueden usar un procesador, aun cuando haya uno disponible. Un subproceso ejecutable puede cambiar al estado en espera sincronizado si proporciona un intervalo de espera opcional cuando está esperando a que otro subproceso realice una tarea. Dicho subproceso regresa al estado ejecutable cuando se lo notifica otro subproceso, o cuando expira el intervalo de tiempo; lo que ocurra primero. Otra manera de colocar a un subproceso en el estado en espera sincronizado es dejar inactivo un subproceso ejecutable. Un subproceso inactivo permanece en el estado en espera sincronizado durante un periodo designado de tiempo (conocido como intervalo de inactividad), después del cual regresa al estado ejecutable. Los subprocesos están inactivos cuando, en cierto momento, no tienen una tarea que realizar. Por ejemplo, un procesador de palabras puede contener un subproceso que periódicamente respalde (es decir, escriba una copia de) el documento actual en el disco, para fines de recuperarlo. Si el subproceso no quedara inactivo entre un respaldo y otro, requeriría un ciclo en el que se evaluara continuamente si debe escribir una copia del documento en el disco. Este ciclo consumiría tiempo del procesador sin realizar un trabajo productivo, con lo cual se reduciría el rendimiento del sistema. En este caso, es más eficiente para el subproceso especificar un intervalo de inactividad (equivalente al periodo entre respaldos sucesivos) y entrar al estado en espera sincronizado. Este subproceso se regresa al estado ejecutable cuando expire su intervalo de inactividad, punto en el cual escribe una copia del documento en el disco y vuelve a entrar al estado en espera sincronizado. Un subproceso ejecutable cambia al estado bloqueado cuando trata de realizar una tarea que no puede completarse inmediatamente, y debe esperar temporalmente hasta que se complete esa tarea. Por ejemplo, cuando un subproceso emite una petición de entrada/salida, el sistema operativo bloquea el subproceso para que no se ejecute sino hasta que se complete la petición de E/S; en ese punto, el subproceso bloqueado cambia al estado ejecutable, para poder continuar su ejecución. Un subproceso bloqueado no puede usar un procesador, aun si hay uno disponible. Un subproceso ejecutable entra al estado terminado (algunas veces conocido como el estado muerto) cuando completa exitosamente su tarea, o termina de alguna otra forma (tal vez debido a un error). En el diagrama

www.elsolucionario.net

23.3 Prioridades y programación de subprocesos

929

de estados de UML de la figura 23.1, el estado terminado va seguido por el estado final de UML (el símbolo de viñeta) para indicar el final de las transiciones de estado. A nivel del sistema operativo, el estado ejecutable de Java generalmente abarca dos estados separados (figura 23.2). El sistema operativo oculta estos estados de la Máquina Virtual de Java (JVM), la cual sólo ve el estado ejecutable. Cuando un subproceso cambia por primera vez al estado ejecutable desde el estado nuevo, el subproceso se encuentra en el estado listo. Un subproceso listo entra al estado en ejecución (es decir, empieza su ejecución) cuando el sistema operativo lo asigna a un procesador; a esto también se le conoce como despachar el subproceso. En la mayoría de los sistemas operativos, a cada subproceso se le otorga una pequeña cantidad de tiempo del procesador (lo cual se conoce como quantum o intervalo de tiempo) en la que debe realizar su tarea. (El proceso de decidir qué tan grande debe ser el quantum de tiempo es un tema clave en los cursos de sistemas operativos). Cuando expira su quantum, el subproceso regresa al estado listo y el sistema operativo asigna otro subproceso al procesador (vea la sección 23.3). Las transiciones entre los estados listo y en ejecución las maneja únicamente el sistema operativo. La JVM no “ve” las transiciones; simplemente ve el subproceso como ejecutable y deja al sistema operativo la decisión de cambiar el subproceso entre listo y ejecutable. El proceso que utiliza un sistema operativo para determinar qué subproceso debe despachar se conoce como programación de subprocesos, y depende de las prioridades de los subprocesos (que veremos en la siguiente sección).

ejecutable

el sistema operativo despacha un subproceso

listo

ejecutable expira quantum

Figura 23.2 | Vista interna del sistema operativo del estado ejecutable de Java.

23.3 Prioridades y programación de subprocesos Todo subproceso en Java tiene una prioridad de subproceso, la cual ayuda al sistema operativo a determinar el orden en el que se programan los subprocesos. Las prioridades de Java varían entre MIN_PRIORITY (una constante de 1) y MAX_PRIORITY (una constante de 10). De manera predeterminada, cada subproceso recibe la prioridad NORM_PRIORITY (una constante de 5). Cada nuevo subproceso hereda la prioridad del subproceso que lo creó. De manera informal, los subprocesos de mayor prioridad son más importantes para un programa, y se les debe asignar tiempo del procesador antes que a los subprocesos de menor prioridad. Sin embargo, las prioridades de los subprocesos no garantizan el orden en el que se ejecutan los subprocesos. [Nota: las constantes (MAX_PRIORITY, MIN_PRIORITY y NORM_PRIORITY) se declaran en la clase Thread. Se recomienda no crear y usar explícitamente objetos Thread para implementar la concurrencia, sino utilizar mejor la interfaz Executor (que describiremos en la sección 23.4.2). La clase Thread contiene varios métodos static útiles, los cuales describiremos más adelante en este capítulo]. La mayoría de los sistemas operativos soportan los intervalos de tiempo (timeslicing), que permiten a los subprocesos de igual prioridad compartir un procesador. Sin el intervalo de tiempo, cada subproceso en un conjunto de subprocesos de igual prioridad se ejecuta hasta completarse (a menos que deje el estado ejecutable y entre al estado en espera o en espera sincronizado, o lo interrumpa un subproceso de mayor prioridad) antes que otros subprocesos de igual prioridad tengan oportunidad de ejecutarse. Con el intervalo de tiempo, aun si un subproceso no ha terminado de ejecutarse cuando expira su quantum, el procesador se quita del subproceso y pasa al siguiente subproceso de igual prioridad, si hay uno disponible. El programador de subprocesos de un sistema operativo determina cuál subproceso se debe ejecutar a continuación. Una implementación simple del programador de subprocesos mantiene el subproceso de mayor prioridad en ejecución en todo momento y, si hay más de un subproceso de mayor prioridad, asegura que todos esos subprocesos se ejecuten durante un quantum cada uno, en forma cíclica (round-robin). En la figura 23.3 se muestra una cola de prioridades multinivel para los subprocesos. En la figura, suponiendo que hay una computadora con un solo procesador, los subprocesos A y B se ejecutan cada uno durante un quantum, en forma cíclica hasta que ambos subprocesos terminan su ejecución. Esto significa que A obtiene un quantum de tiempo

www.elsolucionario.net

930

Capítulo 23

Subprocesamiento múltiple

Subprocesos listos

Thread.MAX_PRIORITY

Prioridad10

A

Prioridad 9

C

B

Prioridad 8

Thread.NORM_PRIORITY

Prioridad 7

D

Prioridad 6

G

Prioridad 5

H

I

J

K

E

F

Prioridad 4

Prioridad 3

Prioridad 2

Thread.MIN_PRIORITY

Prioridad 1

Figura 23.3 | Programación de prioridad de subprocesos. para ejecutarse. Después B obtiene un quantum. Luego A obtiene otro quantum. Después B obtiene otro. Esto continúa hasta que un subproceso se completa. Entonces, el procesador dedica todo su poder al subproceso restante (a menos que esté listo otro subproceso de prioridad 10). A continuación, el subproceso C se ejecuta hasta completarse (suponiendo que no llegan subprocesos de mayor prioridad). Los subprocesos D, E y F se ejecutan cada uno durante un quantum, en forma cíclica hasta que todos terminan de ejecutarse (de nuevo, suponiendo que no llegan procesos de mayor prioridad). Este proceso continúa hasta que todos los subprocesos se ejecutan hasta completarse. Cuando un subproceso de mayor prioridad entra al estado listo, el sistema operativo generalmente sustituye el subproceso actual en ejecución para dar preferencia al subproceso de mayor prioridad (una operación conocida como programación preferente). Dependiendo del sistema operativo, los subprocesos de mayor prioridad

www.elsolucionario.net

23.4 Creación y ejecución de subprocesos

931

podrían posponer (tal vez de manera indefinida) la ejecución de los subprocesos de menor prioridad. Comúnmente, a dicho aplazamiento indefinido se le conoce, en forma más colorida, como inanición. Java cuenta con herramientas de concurrencia de alto nivel para ocultar parte de esta complejidad, y hacer que los programas sean menos propensos a errores. Las prioridades de los subprocesos se utilizan en segundo plano para interactuar con el sistema operativo, pero la mayoría de los programadores que utilizan subprocesamiento múltiple en Java no tienen que preocuparse por configurar y ajustar las prioridades de los subprocesos.

Tip de portabilidad 23.1 La programación de subprocesos es independiente de la plataforma; el comportamiento de un programa con subprocesamiento múltiple podría variar entre las distintas implementaciones de Java.

Tip de portabilidad 23.2 Al diseñar programas con subprocesamiento múltiple, considere las capacidades de subprocesamiento de las plataformas en las que se ejecutarán esos programas. Si utiliza prioridades distintas a las predeterminadas, el comportamiento de sus programas será específico para la plataforma en la que se ejecuten. Si su meta es la portabilidad, no ajuste las prioridades de los subprocesos.

23.4 Creación y ejecución de subprocesos El medio preferido de crear aplicaciones en Java con subprocesamiento múltiple es mediante la implementación de la interfaz Runnable (del paquete java.lang). Un objeto Runnable representa una “tarea” que puede ejecutarse concurrentemente con otras tareas. La interfaz Runnable declara un solo método, run, el cual contiene el código que define la tarea que debe realizar un objeto Runnable. Cuando se crea e inicia un subproceso que ejecuta un objeto Runnable, el subproceso llama al método run del objeto Runnable, el cual se ejecuta en el nuevo subproceso.

23.4.1 Objetos Runnable y la clase Thread La clase TareaImprimir (figura 23.4) implementa a Runnable (línea 5), de manera que puedan ejecutarse varios objetos TareaImprimir en forma concurrente. La variable tiempoInactividad (línea 7) almacena un valor entero aleatorio de 0 a 5 segundos, que se crea en el constructor de TareaImprimir (línea 16). Cada subproceso que ejecuta a un objeto TareaImprimir permanece inactivo durante la cantidad de tiempo especificado por tiempoInactividad, después imprime el nombre de la tarea y un mensaje indicando que terminó su inactividad.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

// Fig. 23.4: TareaImprimir.java // La clase TareaImprimir permanece inactiva durante un tiempo aleatorio entre 0 y 5 segundos import java.util.Random; public class TareaImprimir implements Runnable { private final int tiempoInactividad; // tiempo de inactividad aleatorio para el subproceso private final String nombreTarea; // nombre de la tarea private final static Random generador = new Random(); public TareaImprimir( String nombre ) { nombreTarea = nombre; // establece el nombre de la tarea // elige un tiempo de inactividad aleatorio entre 0 y 5 segundos tiempoInactividad = generador.nextInt( 5000 ); // milisegundos

Figura 23.4 | La clase TareaImprimir permanece inactiva durante un tiempo aleatorio de 0 a 5 segundos. (Parte 1 de 2).

www.elsolucionario.net

932

17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

Capítulo 23

Subprocesamiento múltiple

} // fin del constructor de TareaImprimir // el método run contiene el código que ejecutará un subproceso public void run() { try // deja el subproceso inactivo durante tiempoInactividad segundos { System.out.printf( "%s va a estar inactivo durante %d milisegundos.\n", nombreTarea, tiempoInactividad ); Thread.sleep( tiempoInactividad ); // deja el subproceso inactivo } // fin de try catch ( InterruptedException excepcion ) { System.out.printf( "%s %s\n", nombreTarea, "termino en forma prematura, debido a la interrupcion" ); } // fin de catch // imprime el nombre de la tarea System.out.printf( "%s termino su inactividad\n", nombreTarea ); } // fin del método run } // fin de la clase TareaImprimir

Figura 23.4 | La clase TareaImprimir permanece inactiva durante un tiempo aleatorio de 0 a 5 segundos. (Parte 2 de 2). Un objeto TareaImprimir se ejecuta cuando un subproceso llama al método run de TareaImprimir. En las líneas 24 y 25 se muestra un mensaje, indicando el nombre de la tarea actual en ejecución y que ésta va a quedar inactiva durante tiempoInactividad milisegundos. En la línea 26 se invoca al método static sleep de la clase Thread, para colocar el subproceso en el estado en espera sincronizado durante la cantidad especificada de tiempo. En este punto, el subproceso pierde al procesador y el sistema permite que otro subproceso se ejecute. Cuando el subproceso despierta, vuelve a entrar al estado ejecutable. Cuando el objeto TareaImprimir se asigna a otro procesador de nuevo, en la línea 35 se imprime un mensaje indicando que la tarea dejó de estar inactiva, y después el método run termina. Observe que la instrucción catch en las líneas 28 a 32 es obligatoria, ya que el método sleep podría lanzar una excepción InterruptedException (verificada) si se hace una llamada al método interrupt del subproceso inactivo. En la figura 23.5 se crean objetos Thread para ejecutar objetos TareaImprimir. En las líneas 12 a 14 se crean tres subprocesos, cada uno de los cuales especifica un nuevo objeto TareaImprimir a ejecutar. En las líneas 19 a 21 se hace una llamada al método start de Thread en cada uno de los subprocesos; cada llamada invoca al método run del objeto Runnable correspondiente para ejecutar la tarea. En la línea 23 se imprime un mensaje indicando que se han iniciado todas las tareas de los subprocesos, y que el método main terminó su ejecución.

1 2 3 4 5 6 7 8 9 10 11 12 13 14

// Fig. 23.5: CreadorSubproceso.java // Creación e inicio de tres subprocesos para ejecutar objetos Runnable. import java.lang.Thread; public class CreadorSubproceso { public static void main( String[] args ) { System.out.println( "Creacion de subprocesos" ); // crea cada subproceso con un nuevo Thread subproceso1 = new Thread( new Thread subproceso2 = new Thread( new Thread subproceso3 = new Thread( new

objeto Runnable TareaImprimir( "tarea1" ) ); TareaImprimir( "tarea2" ) ); TareaImprimir( "tarea3" ) );

Figura 23.5 | Creación e inicio de tres subprocesos para ejecutar objetos Runnable. (Parte 1 de 2).

www.elsolucionario.net

23.4 Creación y ejecución de subprocesos

15 16 17 18 19 20 21 22 23 24 25

933

System.out.println( "Subprocesos creados, iniciando tareas." ); // inicia los subprocesos y los coloca en el subproceso1.start(); // invoca al método run subproceso2.start(); // invoca al método run subproceso3.start(); // invoca al método run

estado "en ejecución" de tarea1 de tarea2 de tarea3

System.out.println( "Tareas iniciadas, main termina.\n" ); } // fin de main } // fin de la clase CreadorSubproceso

Creacion de subprocesos Subprocesos creados, iniciando tareas Tareas iniciadas, main termina Tarea3 tarea2 Tarea1 tarea2 tarea3 tarea1

va a estar va a estar va a estar termino su termino su termino su

inactivo durante 491 milisegundos inactivo durante 71 milisegundos inactivo durante 3549 milisegundos inactividad inactividad inactividad

Creacion de subprocesos Subprocesos creados, iniciando tareas tarea1 va a estar inactivo durante 4666 milisegundos tarea2 va a estar inactivo durante 48 milisegundos tarea3 va a estar inactivo durante 3924 milisegundos Tareas iniciadas, main termina subproceso2 termino su inactividad subproceso3 termino su inactividad subproceso1 termino su inactividad

Figura 23.5 | Creación e inicio de tres subprocesos para ejecutar objetos Runnable. (Parte 2 de 2).

El código en el método main se ejecuta en el subproceso principal, un subproceso creado por la JVM. El código en el método run de TareaImprimir (líneas 20 a 36 de la figura 23.4) se ejecuta en los subprocesos creados en las líneas 12 a 14 de la figura 23.5. Cuando termina el método main, el programa en sí continúa ejecutándose, ya que aún hay subprocesos vivos (es decir, alguno de los tres subprocesos que creamos no ha llegado aún al estado terminado). El programa no terminará sino hasta que su último subproceso termine de ejecutarse, punto en el cual la JVM también terminará. Los resultados de ejemplo para este programa muestran el nombre de cada tarea y el tiempo de inactividad, al momento en que el subproceso queda inactivo. El subproceso con el tiempo de inactividad más corto es el que normalmente se despierta primero, indicando que acaba de dejar de estar inactivo y termina. En la sección 23.9 hablaremos sobre las cuestiones acerca del subprocesamiento múltiple que podrían evitar que el subproceso con el tiempo de inactividad más corto se despertara primero. En el primer conjunto de resultados, el subproceso principal termina antes de que cualquiera de los otros subprocesos impriman sus nombres y tiempos de inactividad. Esto muestra que el subproceso principal se ejecuta hasta completarse antes que cualquiera de los otros subprocesos tengan la oportunidad de ejecutarse. En el segundo conjunto de resultados, todos los subprocesos imprimen sus nombres y tiempos de inactividad antes de que termine el subproceso principal. Esto muestra que el sistema operativo permitió a los otros subprocesos ejecutarse antes de que terminara el subproceso principal. Éste es un ejemplo de la programación cíclica (round-robin) que vimos en la sección 23.3. Además, observe que en el primer conjunto de resultados de ejemplo, tarea3 queda inactivo primero y tarea1 queda inactivo al último, aun cuando empezamos el subproceso de tarea1 primero. Esto ilustra el hecho de que no podemos predecir el orden en el que se programarán los subprocesos, aun si conocemos el orden en el que se crearon e iniciaron. Por

www.elsolucionario.net

934

Capítulo 23

Subprocesamiento múltiple

último, en cada uno de los conjuntos de resultados, observe que el orden en el que los subprocesos indican que dejaron de estar inactivos coincide con los tiempos de inactividad de menor a mayor de los tres subprocesos. Aunque éste es el orden razonable e intuitivo para que estos subprocesos terminen sus tareas, no se garantiza que los subprocesos vayan a terminar en este orden.

23.4.2 Administración de subprocesos con el marco de trabajo Executor Aunque es posible crear subprocesos en forma explícita, como en la figura 23.5, se recomienda utilizar la interfaz Executor para administrar la ejecución de objetos Runnable de manera automática. Por lo general, un objeto Executor crea y administra un grupo de subprocesos, al cual se le denomina reserva de subprocesos, para ejecutar objetos Runnable. Usted puede crear sus propios subprocesos, pero el uso de un objeto Executor tiene muchas ventajas. Los objetos Executor pueden reutilizar los subprocesos existentes para eliminar la sobrecarga de crear un nuevo subproceso para cada tarea, y pueden mejorar el rendimiento al optimizar el número de subprocesos, con lo cual se asegura que el procesador se mantenga ocupado, sin crear demasiados subprocesos como para que la aplicación se quede sin recursos. La interfaz Executor declara un solo método llamado execute, el cual acepta un objeto Runnable como argumento. El objeto Executor asigna cada objeto Runnable que se pasa a su método execute a uno de los subprocesos disponibles en la reserva. Si no hay subprocesos disponibles, el objeto Executor crea un nuevo subproceso, o espera a que haya uno disponible y lo asigna al objeto Runnable que se pasó al método execute. La interfaz ExecutorService (del paquete java.util.concurrent) es una interfaz que extiende a Executor y declara varios métodos más para administrar el ciclo de vida de un objeto Executor. Un objeto que implementa a la interfaz ExecutorService se puede crear mediante el uso de los métodos static declarados en la clase Executors (del paquete java.util.concurrent). Utilizaremos la interfaz ExecutorService y un método de la clase Executors en la siguiente aplicación, la cual ejecuta tres tareas. En la figura 23.6 se utiliza un objeto ExecutorService para administrar subprocesos que ejecuten objetos TareaImprimir. El método main (líneas 8 a 29) crea y nombra tres objetos TareaImprimir (líneas 11 a 13). En la línea 18 se utiliza el método newCachedThreadPool de Executors para obtener un objeto ExecutorService que crea nuevos subprocesos, según los va necesitando la aplicación. Estos subprocesos los utiliza ejecutorSubprocesos para ejecutar los objetos Runnable.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

// Fig. 23.6: EjecutorTareas.java // Uso de un objeto ExecutorService para ejecutar objetos Runnable. import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; public class EjecutorTareas { public static void main( String[] args ) { // crea y nombra cada objeto Runnable TareaImprimir tarea1 = new TareaImprimir( "tarea1" ); TareaImprimir tarea2 = new TareaImprimir( "tarea2" ); TareaImprimir tarea3 = new TareaImprimir( "tarea3" ); System.out.println( "Iniciando Executor" ); // crea objeto ExecutorService para administrar los subprocesos ExecutorService ejecutorSubprocesos = Executors.newCachedThreadPool(); // inicia los subprocesos y los coloca ejecutorSubprocesos.execute( tarea1 ); ejecutorSubprocesos.execute( tarea2 ); ejecutorSubprocesos.execute( tarea3 );

en // // //

el estado ejecutable inicia tarea1 inicia tarea2 inicia tarea3

Figura 23.6 | Uso de un objeto ExecutorService para ejecutar objetos Runnable. (Parte 1 de 2).

www.elsolucionario.net

23.5 Sincronización de subprocesos

25 26 27 28 29 30

935

// cierra los subprocesos trabajadores cuando terminan sus tareas ejecutorSubprocesos.shutdown(); System.out.println( "Tareas iniciadas, main termina.\n" ); } // fin de main } // fin de la clase EjecutorTareas

Iniciando Executor Tareas iniciadas, main termina tarea1 tarea2 tarea3 tarea3 tarea2 tarea1

va a estar va a estar va a estar termino su termino su termino su

inactivo durante 4806 milisegundos inactivo durante 2513 milisegundos inactivo durante 1132 milisegundos inactividad inactividad inactividad

Iniciando Executor tarea1 va a estar inactivo durante 1342 milisegundos tarea2 va a estar inactivo durante 277 milisegundos tarea3 va a estar inactivo durante 2737 milisegundos Tareas iniciadas, main termina subproceso2 termino su inactividad subproceso1 termino su inactividad subproceso3 termino su inactividad

Figura 23.6 | Uso de un objeto ExecutorService para ejecutar objetos Runnable. (Parte 2 de 2). En cada una de las líneas 21 a 23 se invoca el método execute de ExecutorService. Este método ejecuta el objeto Runnable que recibe como argumento (en este caso, un objeto TareaImprimir) en algún momento en el futuro. La tarea especificada puede ejecutarse en uno de los subprocesos en la reserva de ExecutorService, en un nuevo subproceso creado para ejecutarla, o en el subproceso que llamó al método execute; el objeto ExecutorService administra estos detalles. El método execute regresa inmediatamente después de cada invocación; el programa no espera a que termine cada objeto TareaImprimir. En la línea 26 se hace una llamada al método shutdown de ExecutorService, el cual notifica al objeto ExecutorService para que deje de aceptar nuevas tareas, pero continúa ejecutando las tareas que ya se hayan enviado. Una vez que se han completado todos los objetos Runnable enviados anteriormente, el objeto ejecutorSubprocesos termina. En la línea 28 se imprime un mensaje indicando que se iniciaron las tareas y que el subproceso principal está terminando su ejecución. Los conjuntos de resultados de ejemplo son similares a los del programa anterior y, de nuevo, demuestran el no determinismo de la programación de subprocesos.

23.5 Sincronización de subprocesos Cuando varios subprocesos comparten un objeto, y éste puede ser modificado por uno o más de los subprocesos, pueden ocurrir resultados indeterminados (como veremos en los ejemplos), a menos que el acceso al objeto compartido se administre de manera apropiada. Si un subproceso se encuentra en el proceso de actualizar a un objeto compartido, y otro subproceso trata de actualizarlo también, no queda claro cuál actualización del subproceso se lleva a cabo. Cuando esto ocurre, el comportamiento del programa no puede determinarse; algunas veces el programa producirá los resultados correctos; sin embargo, en otras ocasiones producirá los resultados incorrectos. En cualquier caso, no habrá indicación de que el objeto compartido se manipuló en forma incorrecta. El problema puede resolverse si se da a un subproceso a la vez el acceso exclusivo al código que manipula al objeto compartido. Durante ese tiempo, otros subprocesos que deseen manipular el objeto deben mantenerse en espera. Cuando el subproceso con acceso exclusivo al objeto termina de manipularlo, a uno de los subprocesos que estaba en espera se le debe permitir que continúe ejecutándose. Este proceso, conocido como sincronización de subprocesos, coordina el acceso a los datos compartidos por varios subprocesos concurrentes. Al sincronizar

www.elsolucionario.net

936

Capítulo 23

Subprocesamiento múltiple

los subprocesos de esta forma, podemos asegurar que cada subproceso que accede a un objeto compartido excluye a los demás subprocesos de hacerlo en forma simultánea; a esto se le conoce como exclusión mutua. Una manera de realizar la sincronización es mediante los monitores integrados en Java. Cada objeto tiene un monitor y un bloqueo de monitor (o bloqueo intrínseco). El monitor asegura que el bloqueo de monitor de su objeto se mantenga por un máximo de sólo un subproceso a la vez. Así, los monitores y los bloqueos de monitor se pueden utilizar para imponer la exclusión mutua. Si una operación requiere que el subproceso en ejecución mantenga un bloqueo mientras se realiza la operación, un subproceso debe adquirir el bloqueo para poder continuar con la operación. Otros subprocesos que traten de realizar una operación que requiera el mismo bloqueo permanecerán bloqueados hasta que el primer subproceso libere el bloqueo, punto en el cual los subprocesos bloqueados pueden tratar de adquirir el bloqueo y continuar con la operación. Para especificar que un subproceso debe mantener un bloqueo de monitor para ejecutar un bloque de código, el código debe colocarse en una instrucción synchronized. Se dice que dicho código está protegido por el bloqueo de monitor; un subproceso debe adquirir el bloqueo para ejecutar las instrucciones synchronized. El monitor sólo permite que un subproceso a la vez ejecute instrucciones dentro de bloques synchronized que se bloqueen en el mismo objeto, ya que sólo un subproceso a la vez puede mantener el bloqueo de monitor. Las instrucciones synchronized se declaran mediante la palabra clave synchronized: synchronized ( {

objeto

)

Instrucciones } // fin de la instrucción synchronized

en donde objeto es el objeto cuyo bloqueo de monitor se va a adquirir; generalmente, objeto es this si es el objeto en el que aparece la instrucción synchronized. Si varias instrucciones synchronized están tratando de ejecutarse en un objeto al mismo tiempo, sólo una de ellas puede estar activa en el objeto; todos los demás subprocesos que traten de entrar a una instrucción synchronized en el mismo objeto se colocan en el estado bloqueado. Cuando una instrucción synchronized termina de ejecutarse, el bloqueo de monitor del objeto se libera y el sistema operativo puede permitir que uno de los subprocesos bloqueados, que intentan entrar a una instrucción synchronized, adquieran el bloqueo para continuar. Java también permite los métodos synchronized. Dicho método es equivalente a una instrucción synchronized que encierra el cuerpo completo de un método, y que utiliza a this como el objeto cuyo bloqueo de monitor se va a adquirir. Puede especificar un método como synchronized; para ello, coloque la palabra clave synchronized antes del tipo de valor de retorno del método en su declaración.

23.5.1 Cómo compartir datos sin sincronización Ahora presentaremos un ejemplo para ilustrar los peligros de compartir un objeto entre varios subprocesos sin una sincronización apropiada. En este ejemplo, dos objetos Runnable mantienen referencias a un solo arreglo entero. Cada objeto Runnable escribe cinco valores en el arreglo, y después termina. Tal vez esto parezca inofensivo, pero puede provocar errores si el arreglo se manipula sin sincronización.

La clase ArregloSimple Un objeto de la clase ArregloSimple (figura 23.7) se compartirá entre varios subprocesos. ArregloSimple permitirá que esos subprocesos coloquen valores int en arreglo (declarado en la línea 7). En la línea 8 se inicializa la variable indiceEscritura, la cual se utilizará para determinar el elemento del arreglo en el que se debe escribir a continuación. El constructor (líneas 12 a 15) crea un arreglo entero del tamaño deseado.

1 2 3 4 5 6

// Fig. 23.7: ArregloSimple.java // Clase que administra un arreglo simple para compartirlo entre varios subprocesos. import java.util.Random; public class ArregloSimple // PRECAUCIÓN: ¡NO ES SEGURO PARA LOS SUBPROCESOS! {

Figura 23.7 | Clase que administra un arreglo entero para compartirlo entre varios subprocesos. (Parte 1 de 2).

www.elsolucionario.net

23.5 Sincronización de subprocesos

7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51

937

private final int arreglo[]; // el arreglo entero compartido private int indiceEscritura = 0; // índice del siguiente elemento a escribir private final static Random generador = new Random(); // construye un objeto ArregloSimple de un tamaño dado public ArregloSimple( int tamanio ) { arreglo = new int[ tamanio ]; } // fin del constructor // agrega un valor al arreglo compartido public void agregar( int valor ) { int posicion = indiceEscritura; // almacena el índice de escritura try { // pone el subproceso en inactividad de 0 a 499 milisegundos Thread.sleep( generador.nextInt( 500 ) ); } // fin de try catch ( InterruptedException ex ) { ex.printStackTrace(); } // fin de catch // coloca el valor en el elemento apropiado arreglo[ posicion ] = valor; System.out.printf( "%s escribio %2d en el elemento %d.\n", Thread.currentThread().getName(), valor, posicion ); ++indiceEscritura; // incrementa el índice del siguiente elemento a escribir System.out.printf( "Siguiente indice de escritura: %d\n", indiceEscritura ); } // fin del método agregar // se utiliza para imprimir el contenido del arreglo entero compartido public String toString() { String cadenaArreglo = "\nContenido de ArregloSimple:\n"; for ( int i = 0; i < arreglo.length; i++ ) cadenaArreglo += arreglo[ i ] + " "; return cadenaArreglo; } // fin del método toString } // fin de la clase ArregloSimple

Figura 23.7 | Clase que administra un arreglo entero para compartirlo entre varios subprocesos. (Parte 2 de 2).

El método agregar (líneas 18 a 39) permite insertar nuevos valores al final del arreglo. En la línea 20 se almacena el valor de indiceEscritura actual. En la línea 25, el subproceso que invoca a agregar queda inactivo durante un intervalo aleatorio de 0 a 499 milisegundos. Esto se hace para que los problemas asociados con el acceso desincronizado a los datos compartidos sean más obvios. Una vez que el subproceso deja de estar inactivo, en la línea 33 se inserta el valor que se pasa a agregar en el arreglo, en el elemento especificado por posicion. En las líneas 34 y 35 se imprime un mensaje, indicando el nombre del subproceso en ejecución, el valor que se insertó en el arreglo y en dónde se insertó. En la línea 37 se incrementa indiceEscritura, de manera que la siguiente llamada a agregar insertará un valor en el siguiente elemento del arreglo. En las líneas 42 a 50 se sobrescribe el método toString para crear una representación String del contenido del arreglo.

www.elsolucionario.net

938

Capítulo 23

Subprocesamiento múltiple

La clase EscritorArreglo La clase EscritorArreglo (figura 23.8) implementa a la interfaz Runnable para definir una tarea para insertar valores en un objeto ArregloSimple. El constructor (líneas 10 a 14) recibe dos argumentos: un valor entero, que viene siendo el primer valor que insertará esta tarea en el objeto ArregloSimple, y una referencia al objeto ArregloSimple. En la línea 20 se invoca el método agregar en el objeto ArregloSimple. La tarea se completa una vez que se agregan tres enteros consecutivos, empezando con valorInicial, al objeto ArregloSimple.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

// Fig. 23.8: EscritorArreglo.java // Agrega enteros a un arreglo compartido con otros objetos Runnable import java.lang.Runnable; public class EscritorArreglo implements Runnable { private final ArregloSimple arregloSimpleCompartido; private final int valorInicial; public EscritorArreglo( int valor, ArregloSimple arreglo ) { valorInicial = valor; arregloSimpleCompartido= arreglo; } // fin del constructor public void run() { for ( int i = valorInicial; i < valorInicial + 3; i++ ) { arregloSimpleCompartido.agregar( i ); // agrega un elemento al arreglo compartido } // fin de for } // fin del método run } // fin de la clase EscritorArreglo

Figura 23.8 | Agrega enteros a un arreglo compartido con otros objetos Runnable.

La clase PruebaArregloCompartido La clase PruebaArregloCompartido ejecuta dos tareas EscritorArreglo que agregan valores a un solo objeto ArregloSimple. En la línea 12 se construye un objeto ArregloSimple con seis elementos. En las líneas 15 y 16 se crean dos nuevas tareas EscritorArreglo, una que coloca los valores 1 a 3 en el objeto ArregloSimple, y una que coloca los valores 11 a 13. En las líneas 19 a 21 se crea un objeto ExecutorService y se ejecutan los dos objetos EscritorArreglo. En la línea 23 se invoca el método shutDown de ExecutorService para evitar que se inicien tareas adicionales, y para permitir que la aplicación termine cuando las tareas actuales en ejecución se completen.

1 2 3 4 5 6 7 8 9 10

// Fig 23.9: PruebaArregloCompartido.java // Ejecuta dos objetos Runnable para agregar elementos a un objeto ArregloSimple compartido. import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; public class PruebaArregloCompartido { public static void main( String[] arg ) {

Figura 23.9 | Ejecuta dos objetos Runnable para insertar valores en un arreglo compartido. (Parte 1 de 2).

www.elsolucionario.net

23.5 Sincronización de subprocesos

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

939

// construye el objeto compartido ArregloSimple arregloSimpleCompartido = new ArregloSimple( 6 ); // crea dos tareas para escribir en el objeto ArregloSimple compartido EscritorArreglo escritor1 = new EscritorArreglo( 1, arregloSimpleCompartido ); EscritorArreglo escritor2 = new EscritorArreglo( 11, arregloSimpleCompartido ); // ejecuta las tareas con un objeto ExecutorService ExecutorService ejecutor = Executors.newCachedThreadPool(); ejecutor.execute( escritor1 ); ejecutor.execute( escritor2 ); ejecutor.shutdown(); try { // espera 1 minuto para que ambos escritores terminen de ejecutarse boolean tareasTerminaron = ejecutor.awaitTermination( 1, TimeUnit.MINUTES ); if ( tareasTerminaron ) System.out.println( arregloSimpleCompartido ); // imprime el contendio else System.out.println( "Se agoto el tiempo esperando a que las tareas terminaran." ); } // fin de try catch ( InterruptedException ex ) { System.out.println( "Hubo una interrupcion mientras esperaba a que terminaran las tareas." ); } // fin de catch } // fin de main } // fin de la clase PruebaArregloCompartido

pool-1-thread-1 escribio 1 en Siguiente indice de escritura: pool-1-thread-1 escribio 2 en Siguiente indice de escritura: pool-1-thread-1 escribio 3 en Siguiente indice de escritura: pool-1-thread-2 escribio 11 en Siguiente indice de escritura: pool-1-thread-2 escribio 12 en Siguiente indice de escritura: pool-1-thread-2 escribio 13 en Siguiente indice de escritura:

el 1 el 2 el 3 el 4 el 5 el 6

elemento 0. elemento 1. elemento 2.

Primero pool-1-thread-1 escribió el valor 1 en el elemento 0. Después pool-1-thread-2 escribió el valor 11 en el elemento 0, con lo cual sobrescribió el valor previamente almacenado.

elemento 0. elemento 4. elemento 5.

Contenido de ArregloSimple: 11 2 3 0 12 13

Figura 23.9 | Ejecuta dos objetos Runnable para insertar valores en un arreglo compartido. (Parte 2 de 2). Recuerde que el método shutdown de ExecutorService regresa de inmediato. Por ende, cualquier código que aparezca después de la llamada al método shutdown de ExecutorService en la línea 23 seguirá ejecutándose, siempre y cuando el subproceso main siga asignado a un procesador. Nos gustaría imprimir el objeto ArregloSimple para mostrarle los resultados después de que los subprocesos completan sus tareas. Entonces, necesitamos que el programa espere a que los subprocesos se completen antes de que main imprima el contenido del objeto ArregloSimple. La interfaz ExecutorService proporciona el método awaitTermination para este fin. Este método devuelve el control al que lo llamó, ya sea cuando se completen todas las tareas que se ejecutan

www.elsolucionario.net

940

Capítulo 23

Subprocesamiento múltiple

en el objeto ExecutorService, o cuando se agote el tiempo de inactividad especificado. Si todas las tareas se completan antes de que se agote el tiempo de awaitTermination, este método devuelve true; en caso contrario devuelve false. Los dos argumentos para awaitTermination representan un valor de límite de tiempo y una unidad de medida especificada con una constante de la clase TimeUnit (en este caso, TimeUnit.MINUTES). En este ejemplo, si ambas tareas se completan antes de que se agote el tiempo de awaitTermination, en la línea 32 se muestra el contenido del objeto ArregloSimple. En caso contrario, en las líneas 34 y 35 se imprime un mensaje indicando que las tareas no terminaron de ejecutarse antes de que se agotara el tiempo de awaitTermination. Los resultados en la figura 23.9 demuestran los problemas (resaltados en la salida) que se pueden producir debido a la falla en la sincronización del acceso a los datos compartidos. El valor 1 se escribió para el elemento 0, y más adelante lo sobrescribió el valor 11. Además, cuando indiceEscritura se incrementó a 3, no se escribió nada en ese elemento, como lo indica el 0 en ese elemento del arreglo impreso. Recuerde que hemos agregado llamadas al método sleep de Thread entre las operaciones con los datos compartidos para enfatizar la imprevisibilidad de la programación de subprocesos, y para incrementar la probabilidad de producir una salida errónea. Es importante observar que, aun si estas operaciones pudieran proceder a su ritmo normal, de todas formas veríamos errores en la salida del programa. Sin embargo, los procesadores modernos pueden manejar las operaciones simples del método agregar de ArregloSimple con tanta rapidez que tal vez no veríamos los errores ocasionados por los dos subprocesos que ejecutan este método en forma concurrente, aun si probáramos el programa docenas de veces. Uno de los retos de la programación con subprocesamiento múltiple es detectar los errores; pueden ocurrir con tan poca frecuencia que un programa erróneo no producirá resultados incorrectos durante la prueba, creando la ilusión de que el programa es correcto.

23.5.2 Cómo compartir datos con sincronización: hacer las operaciones atómicas Los errores de la salida de la figura 23.9 pueden atribuirse al hecho de que el objeto compartido (ArregloSimple) no es seguro para los subprocesos; ArregloSimple es susceptible a errores si varios subprocesos lo utilizan en forma concurrente. El problema recae en el método agregar, el cual almacena el valor de indiceEscritura, coloca un nuevo valor en ese elemento y después incrementa a indiceEscritura. Dicho método no presentaría problemas en un programa con un solo subproceso. No obstante, si un subproceso obtiene el valor de indiceEscritura, no hay garantía de que otro subproceso no pueda llegar e incrementar indiceEscritura antes de que el primer subproceso haya tenido la oportunidad de colocar un valor en el arreglo. Si esto ocurre, el primer subproceso estará escribiendo en el arreglo, con base en un valor pasado de indiceEscritua; un valor que ya no sea válido. Otra posibilidad es que un subproceso podría obtener el valor de indiceEscritura después de que otro subproceso agregue un elemento al arreglo, pero antes de que se incremente indiceEscritura. En este caso también, el primer subproceso escribiría en el arreglo con base en un valor inválido para indiceEscritura. ArregloSimple no es seguro para los subprocesos, ya que permite que cualquier número de subprocesos lean y modifiquen los datos en forma concurrente, lo cual puede producir errores. Para que ArregloSimple sea seguro para los subprocesos, debemos asegurar que no haya dos subprocesos que puedan acceder a este objeto al mismo tiempo. También debemos asegurar que mientras un subproceso se encuentre almacenando indiceEscritura, agregando un valor al arreglo e incrementando indiceEscritura, ningún otro subproceso pueda leer o cambiar el valor de indiceEscritura, o modificar el contenido del arreglo en ningún punto durante estas tres operaciones. En otras palabras, deseamos que estas tres operaciones (almacenar indiceEscritura, escribir en el arreglo, incrementar indiceEscritura) sean una operación atómica, la cual no puede dividirse en suboperaciones más pequeñas. Aunque ningún procesador puede llevar a cabo las tres etapas del método agregar en un solo ciclo de reloj para que la operación sea verdaderamente atómica, podemos simular la atomicidad al asegurar que sólo un subproceso lleve a cabo las tres operaciones al mismo tiempo. Cualquier otro subproceso que necesite realizar la operación deberá esperar hasta que el primer subproceso haya terminado la operación agregar en su totalidad. La atomicidad se puede lograr mediante el uso de la palabra clave synchronized. Al colocar nuestras tres suboperaciones en una instrucción synchronized o en un método synchronized, sólo un subproceso a la vez podrá adquirir el bloqueo y realizar las operaciones. Cuando ese subproceso haya completado todas las operaciones en el bloque synchronized y libere el bloqueo, otro subproceso podrá adquirir el bloqueo y empezar a ejecutar las operaciones. Esto asegura que un subproceso que ejecuta las operaciones pueda ver los valores actuales de los datos compartidos, y que estos valores no puedan cambiar de manera inesperada, en medio de las operaciones y como resultado de que otro subproceso los modifique.

www.elsolucionario.net

23.5 Sincronización de subprocesos

941

Observación de ingeniería de software 23.1 Coloque todos los accesos para los datos mutables que puedan compartir varios subprocesos dentro de instrucciones synchronized, o dentro de métodos synchronized que puedan sincronizarse con el mismo bloqueo. Al realizar varias operaciones con datos compartidos, mantenga el bloqueo durante toda la operación para asegurar que ésta sea, en efecto, atómica.

En la figura 23.10 se muestra la clase ArregloSimple con la sincronización apropiada. Observe que es idéntica a la clase ArregloSimple de la figura 23.7, excepto que agregar es ahora un método synchronized (línea 19). Por lo tanto, sólo un subproceso a la vez puede ejecutar este método. Reutilizaremos las clases EscritorArreglo (figura 23.8) y PruebaArregloCompartido (figura 23.9) del ejemplo anterior.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

// Fig. 23.10: ArregloSimple.java // Clase que administra un arreglo simple para compartirlo entre varios subprocesos. import java.util.Random; public class ArregloSimple { private final int arreglo[]; // el arreglo entero compartido private int indiceEscritura = 0; // índice del siguiente elemento a escribir private final static Random generador = new Random(); // construye un objeto ArregloSimple de un tamaño dado public ArregloSimple( int tamanio ) { arreglo = new int[ tamanio ]; } // fin del constructor // agrega un valor al arreglo compartido public synchronized void agregar( int valor ) { int posicion = indiceEscritura; // almacena el índice de escritura try { // pone el subproceso en inactividad de 0 a 499 milisegundos Thread.sleep( generador.nextInt( 500 ) ); } // fin de try catch ( InterruptedException ex ) { ex.printStackTrace(); } // fin de catch // coloca el valor en el elemento apropiado arreglo[ posicion ] = valor; System.out.printf( "%s escribio %2d en el elemento %d.\n", Thread.currentThread().getName(), valor, posicion ); ++indiceEscritura; // incrementa el índice del siguiente elemento a escribir System.out.printf( "Siguiente indice de escritura: %d\n", indiceEscritura ); } // fin del método agregar // se utiliza para imprimir el contenido del arreglo entero compartido public String toString() { String cadenaArreglo = "\nContenido de ArregloSimple:\n";

Figura 23.10 | Clase que administra un arreglo simple para compartirlo entre varios subprocesos con sincronización. (Parte 1 de 2).

www.elsolucionario.net

942

45 46 47 48 49 50 51

Capítulo 23

Subprocesamiento múltiple

for ( int i = 0; i < arreglo.length; i++ ) cadenaArreglo += arreglo[ i ] + " "; return cadenaArreglo; } // fin del método toString } // fin de la clase ArregloSimple

pool-1-thread-1 escribio 1 en el elemento 0. Siguiente indice de escritura: 1 pool-1-thread-2 escribio 11 en el elemento 1. Siguiente indice de escritura: 2 pool-1-thread-2 escribio 12 en el elemento 2. Siguiente indice de escritura: 3 pool-1-thread-2 escribio 13 en el elemento 3. Siguiente indice de escritura: 4 pool-1-thread-1 escribio 2 en el elemento 4. Siguiente indice de escritura: 5 pool-1-thread-1 escribio 3 en el elemento 5. Siguiente indice de escritura: 6 Contenido de ArregloSimple: 1 11 12 13 2 3

Figura 23.10 | Clase que administra un arreglo simple para compartirlo entre varios subprocesos con sincronización. (Parte 2 de 2).

En la línea 18 se declara el método como synchronized, lo cual hace que todas las operaciones en este método se comporten como una sola operación atómica. En la línea 20 se realiza la primera suboperación: almacenar el valor de indiceEscritura. En la línea 33 se define la segunda suboperación, escribir un elemento en el elemento que está en el índice posicion. En la línea 37 se incrementa indiceEscritura. Cuando el método termina de ejecutarse en la línea 39, el subproceso en ejecución libera el bloqueo de ArregloSimple, lo cual hace posible que otro subproceso empiece a ejecutar el método agregar. En el método add sincronizado mediante la palabra clave synchronized, imprimimos mensajes en la consola, indicando el progreso de los subprocesos a medida que ejecutan este método, además de realizar las operaciones actuales requeridas para insertar un valor en el arreglo. Hacemos esto de manera que los mensajes se impriman en el orden correcto, con lo cual usted podrá ver si el método está sincronizado apropiadamente, al comparar estos resultados con los del ejemplo anterior, sin sincronización. En ejemplos posteriores seguiremos imprimiendo mensajes de bloques synchronized para fines demostrativos; sin embargo, las operaciones de E/S no deben realizarse comúnmente en bloques synchronized, ya que es importante minimizar la cantidad de tiempo que un objeto permanece “bloqueado”.

Tip de rendimiento 23.2 Mantenga la duración de las instrucciones synchronized lo más corta posible, mientras mantiene la sincronización necesaria. Esto minimiza el tiempo de espera para los subprocesos bloqueados. Evite realizar operaciones de E/S, cálculos extensos y operaciones que no requieran sincronización, manteniendo un bloqueo.

Otra observación en relación con la seguridad de los subprocesos: hemos dicho que es necesario sincronizar el acceso a todos los datos que puedan compartirse entre varios subprocesos. En realidad, esta sincronización es necesaria sólo para los datos mutables, o los datos que puedan cambiar durante su tiempo de vida. Si los datos compartidos no van a cambiar en un programa con subprocesamiento múltiple, entonces no es posible que un subproceso vea valores antiguos o incorrectos, como resultado de que otro subproceso manipule esos datos. Al compartir datos inmutables entre subprocesos, declare los correspondientes campos de datos como final para indicar que los valores de las variables no cambiarán una vez que se inicialicen. Esto evita que los datos compartidos se modifiquen por accidente más adelante en un programa, lo cual podría comprometer la seguridad

www.elsolucionario.net

23.6

Relación productor/consumidor sin sincronización

943

de los subprocesos. Al etiquetar las referencias a los objetos como final se indica que la referencia no cambiará, pero no se garantiza que el objeto en sí sea inmutable; esto depende por completo de las propiedades del objeto. Sin embargo, aún es buena práctica marcar las referencias que no cambiarán como final, ya que esto obliga al constructor del objeto a ser atómico; el objeto estará construido por completo con todos sus campos inicializados, antes de que el programa lo utilice.

Buena práctica de programación 23.1 Siempre declare los campos de datos que no espera modificar como final. Las variables primitivas que se declaran como final pueden compartirse de manera segura entre los subprocesos. La referencia a un objeto que se declara como final asegura que el objeto al que refiere estará completamente construido e inicializado antes de que el programa lo utilice; además, esto evita que la referencia apunte a otro objeto.

23.6 Relación productor/consumidor sin sincronización En una relación productor/consumidor, la porción correspondiente al productor de una aplicación genera datos y los almacena en un objeto compartido, y la porción correspondiente al consumidor de una aplicación lee esos datos del objeto compartido. La relación productor/consumidor separa la tarea de identificar el trabajo que se va a realizar de las tareas involucradas en realizar ese trabajo. Un ejemplo de una relación productor/consumidor común es la cola de impresión. Aunque una impresora podría no estar disponible si queremos imprimir de una aplicación (el productor), aún podemos “completar” la tarea de impresión, ya que los datos se colocan temporalmente en el disco hasta que la impresora esté disponible. De manera similar, cuando la impresora (consumidor) está disponible, no tiene que esperar hasta que un usuario desee imprimir. Los trabajos en la cola de impresión pueden imprimirse tan pronto como la impresora esté disponible. Otro ejemplo de la relación productor/consumidor es una aplicación que copia datos en CDs, colocándolos en un búfer de tamaño fijo, el cual se vacía a medida que la unidad de CD-RW “quema” los datos en el CD. En una relación productor/consumidor con subprocesamiento múltiple, un subproceso productor genera los datos y los coloca en un objeto compartido, llamado búfer. Un subproceso consumidor lee los datos del búfer. Esta relación requiere sincronización para asegurar que los valores se produzcan y se consuman de manera apropiada. Todas las operaciones con los datos mutables que comparten varios subprocesos (es decir, los datos en el búfer) deben protegerse con un bloqueo para evitar la corrupción, como vimos en la sección 23.5. Las operaciones con los datos del búfer compartidos por un subproceso productor y un subproceso consumidor son también dependientes del estado; las operaciones deben proceder sólo si el búfer se encuentra en el estado correcto. Si el búfer se encuentra en un estado en el que no esté completamente lleno, el productor puede producir; si el búfer se encuentra en un estado en el que no esté completamente vacío, el consumidor puede consumir. Todas las operaciones que acceden al búfer deben usar la sincronización para asegurar que los datos se escriban en el búfer, o se lean del búfer, sólo si éste se encuentra en el estado apropiado. Si el productor que trata de colocar los siguientes datos en el búfer determina que éste se encuentra lleno, el subproceso productor debe esperar hasta que haya espacio para escribir un nuevo valor. Si un subproceso consumidor descubre que el búfer está vacío, o que ya se han leído los datos anteriores, también debe esperar a que haya nuevos datos disponibles. Considere cómo pueden surgir errores lógicos si no sincronizamos el acceso entre varios subprocesos que manipulan datos compartidos. Nuestro siguiente ejemplo (figuras 23.11 a 23.15) implementa una relación productor/consumidor sin la sincronización apropiada. Un subproceso productor escribe los números del 1 al 10 en un búfer compartido: una sola ubicación de memoria compartida entre dos subprocesos (en este ejemplo, una sola variable int llamada bufer en la línea 6 de la figura 23.14). El subproceso consumidor lee estos datos del búfer compartido y los muestra en pantalla. La salida del programa muestra los valores que el productor escribe (produce) en el búfer compartido, y los valores que el consumidor lee (consume) del búfer compartido. Cada valor que el subproceso productor escribe en el búfer compartido lo debe consumir exactamente una vez el subproceso consumidor. Sin embargo y por error, los subprocesos en este ejemplo no están sincronizados. Por lo tanto, los datos se pueden perder o desordenar si el productor coloca nuevos datos en el búfer compartido antes de que el consumidor lea los datos anteriores. Además, los datos pueden duplicarse incorrectamente si el consumidor consume datos de nuevo, antes de que el productor produzca el siguiente valor. Para mostrar estas posibilidades, el subproceso consumidor en el siguiente ejemplo mantiene un total de todos los valores que lee. El subproceso productor produce valores del 1 al 10. Si el consumidor lee cada valor producido una, y sólo una vez, el total será de 55. No obstante, si ejecuta este programa varias veces, podrá ver que el total no es siempre 55

www.elsolucionario.net

944

Capítulo 23

Subprocesamiento múltiple

(como se muestra en los resultados de la figura 23.10). Para enfatizar este punto, los subprocesos productor y consumidor en el ejemplo quedan inactivos durante intervalos aleatorios de hasta tres segundos, entre la realización de sus tareas. Por ende, no sabemos cuándo el subproceso productor tratará de escribir un nuevo valor, o cuándo el subproceso consumidor tratará de leer uno. El programa consiste en la interfaz Bufer (figura 23.11) y cuatro clases: Productor (figura 23.12), Consumidor (figura 23.13), BuferSinSincronizacion (figura 23.14) y PruebaBuferCompartido (figura 23.15). La interfaz Bufer declara los métodos establecer (línea 6) y obtener (línea 9) que un objeto Bufer debe implementar para permitir que el subproceso Productor coloque un valor en el Bufer y el proceso Consumidor obtenga un valor del Bufer, respectivamente. Algunos programadores prefieren llamar a estos métodos poner (put) y tomar (take), respectivamente. En posteriores ejemplos, los métodos establecer y obtener llamarán métodos que lanzan excepciones InterruptedException. Aquí declaramos a cada uno de estos métodos con una cláusula throws, para no tener que modificar esta interfaz para los ejemplos posteriores. En la figura 23.14 se muestra la implementación de esta interfaz.

1 2 3 4 5 6 7 8 9 10

// Fig. 23.11: Bufer.java // La interfaz Bufer especifica los métodos que el Productor y el Consumidor llaman. public interface Bufer { // coloca valor int value en Bufer public void establecer( int valor ) throws InterruptedException; // obtiene valor int de Bufer public int obtener() throws InterruptedException; } // fin de la interfaz Bufer

Figura 23.11 | La interfaz Bufer especifica los métodos que el Productor y el Consumidor llaman.

La clase Productor (figura 23.12) implementa a la interfaz Runnable, lo cual le permite ejecutarse como una tarea en un subproceso separado. El constructor (líneas 11 a 14) inicializa la referencia Bufer llamada ubicacionCompartida con un objeto creado en main (línea 14 de la figura 23.15), y que se pasa al constructor en el parámetro compartido. Como veremos, éste es un objeto BuferSinSincronizacion que implementa a la interfaz Bufer sin sincronizar el acceso al objeto compartido. El subproceso Productor en este programa ejecuta las tareas especificadas en el método run (líneas 17 a 39). Cada iteración del ciclo (líneas 21 a 35) invoca al método sleep de Thread (línea 25) para colocar el subproceso Productor en el estado en espera sincronizado durante un intervalo de tiempo aleatorio entre 0 y 3 segundos. Cuando el subproceso despierta, en la línea 26 se pasa el valor de la variable de control cuenta al método establecer del objeto Bufer para establecer el valor del búfer compartido. En la línea 27 se mantiene un total de todos los valores producidos hasta ahora, y en la línea 28 se imprime ese valor. Cuando el ciclo termina, en las líneas 37 y 38 se muestra un mensaje indicando que el Productor ha dejado de producir datos, y está terminando. A continuación, el método run termina, lo cual indica que el Productor completó su tarea. Es importante observar que cualquier método que se llama desde el método run de Runnable (por ejemplo, el método establecer de Bufer) se ejecuta como parte del subproceso de ejecución de esa tarea. Este hecho se vuelve importante en la sección 23.7, en donde agregamos la sincronización a la relación productor/consumidor.

1 2 3 4 5 6

// Fig. 23.12: Productor.java // Productor con un método run que inserta los valores del 1 al 10 en el búfer. import java.util.Random; public class Productor implements Runnable {

Figura 23.12 |

Productor

con un método run que inserta los valores del 1 al 10 en el búfer. (Parte 1 de 2).

www.elsolucionario.net

23.6

7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

Relación productor/consumidor sin sincronización

945

private final static Random generador = new Random(); private final Bufer ubicacionCompartida; // referencia al objeto compartido // constructor public Productor( Bufer compartido ) { ubicacionCompartida = compartido; } // fin del constructor de Productor // almacena valores del 1 al 10 en ubicacionCompartida public void run() { int suma = 0; for ( int cuenta = 1; cuenta { private final Random generador = new Random(); private final JTextArea intermedioJTextArea; // muestra los números primos encontrados private final JButton obtenerPrimosJButton; private final JButton cancelarJButton; private final JLabel estadoJLabel; // muestra el estado del cálculo private final boolean primos[]; // arreglo booleano para buscar números primos private boolean detenido = false; // bandera que indica la cancelación // constructor public CalculadoraPrimos( int max, JTextArea intermedio, JLabel estado, JButton obtenerPrimos, JButton cancel ) { intermedioJTextArea = intermedio;

Figura 23.27 | Calcula los primeros n números primos, y los muestra a medida que los va encontrando. (Parte 1 de 3).

www.elsolucionario.net

23.11

26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83

Subprocesamiento múltiple con GUIs

977

estadoJLabel = estado; obtenerPrimosJButton = obtenerPrimos; cancelarJButton = cancel; primos = new boolean[ max ]; // inicializa todos los valores del arreglo primos con true for ( int i = 0; i < max; i ++ ) primos[ i ] = true; } // fin del constructor // busca todos los números primos hasta max, usando la Criba de Eratóstenes public Integer doInBackground() { int cuenta = 0; // la cantidad de números primos encontrados // empezando en el tercer valor, itera a través del arreglo y pone // false como el valor de cualquier número mayor que sea múltiplo for ( int i = 2; i < primos.length; i++ ) { if ( detenido ) // si se canceló un cálculo return cuenta; else { setProgress( 100 * ( i + 1 ) / primos.length ); try { Thread.currentThread().sleep( generador.nextInt( 5 ) ); } // fin de try catch ( InterruptedException ex ) { estadoJLabel.setText( "Se interrumpio subproceso Trabajador" ); return cuenta; } // fin de catch if ( primos[ i ] ) // i es primo { publish( i ); // hace a i disponible para mostrarlo en la lista de primos ++cuenta; for ( int j = i + i; j < primos.length; j += i ) primos[ j ] = false; // i no es primo } // fin de if } // fin de else } // fin de for return cuenta; } // fin del método doInBackground // muestra los valores publicados en la lista de números primos protected void process( List< Integer > valsPublicados ) { for ( int i = 0; i < valsPublicados.size(); i++ ) intermedioJTextArea.append( valsPublicados.get( i ) + "\n" ); } // fin del método process // código a ejecutar cuando se completa doInBackground protected void done()

Figura 23.27 | Calcula los primeros n números primos, y los muestra a medida que los va encontrando. (Parte 2 de 3).

www.elsolucionario.net

978

84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113

Capítulo 23

Subprocesamiento múltiple

{ obtenerPrimosJButton.setEnabled( true ); // habilita el botón Obtener primos cancelarJButton.setEnabled( false ); // deshabilita el botón Cancelar int numPrimos; try { numPrimos = get(); // obtiene el valor de retorno de doInBackground } // fin de try catch ( InterruptedException ex ) { estadoJLabel.setText( "Se interrumpio mientras se esperaban los resultados." ); return; } // fin de catch catch ( ExecutionException ex ) { estadoJLabel.setText( "Error al realizar el calculo." ); return; } // fin de catch estadoJLabel.setText( "Se encontraron " + numPrimos + " primos." ); } // fin del método done // establece la bandera para dejar de buscar números primos public void detenerCalculo() { detenido = true; } // fin del método detenerCalculo } // fin de la clase CalculadoraPrimos

Figura 23.27 | Calcula los primeros n números primos, y los muestra a medida que los va encontrando. (Parte 3 de 3). La clase CalculadoraPrimos extiende a SwingWorker (línea 11); el primer parámetro de tipo indica el tipo de valor de retorno del método doInBackground y el segundo indica el tipo de los resultados intermedios que se pasan entre los métodos publish y process. En este caso, ambos parámetros de tipo son objetos Integer. El constructor (líneas 22 a 34) recibe como argumentos un entero que indica el límite superior de los números primos a localizar, un objeto JTextArea que se utiliza para mostrar los números primos en la GUI, un objeto JButton para iniciar un cálculo y otro para cancelarlo, y un objeto JLabel para mostrar el estado del cálculo. En las líneas 32 y 33 se inicializan con true los elementos del arreglo boolean llamado primos. CalculadoraPrimos utiliza este arreglo y el algoritmo de la Criba de Eratóstenes (descrito en el ejercicio 7.27) para buscar todos los números primos menores que max. La Criba de Eratóstenes recibe una lista de números naturales de cualquier longitud y, empezando con el primer número primo, filtra todos sus múltiplos. Después avanza al siguiente número primo, que será el siguiente número que no esté filtrado todavía y elimina a todos sus múltiplos. Continúa hasta llegar al final de la lista y cuando se han filtrado todos los números que no son primos. En términos del algoritmo, empezamos con el elemento 2 del arreglo boolean y establecemos en false las celdas correspondientes a todos los valores que sean múltiplos de 2, para indicar que pueden dividirse entre 2 y por ende, no son primos. Después avanzamos al siguiente elemento del arreglo, verificamos si es true y, de ser así, establecemos en false todos sus múltiplos para indicar que pueden dividirse entre el índice actual. Cuando se ha recorrido todo el arreglo de esta forma, todos los índices que contienen true son primos, ya que no tienen divisores. En el método doInBackground (líneas 37 a 73), la variable de control i para el ciclo (líneas 43 a 70) controla el índice actual para implementar la Criba de Eratóstenes. En la línea 45 se evalúa la bandera boolean llamada detenido, la cual indica si el usuario hizo clic en el botón Cancelar. Si detenido es true, el método devuelve la cantidad de números primos encontrados hasta ese momento (línea 46) sin terminar el cálculo. Si no se cancela el cálculo, en la línea 49 se hace una llamada al método setProgress para actualizar la propiedad de progreso con el porcentaje del arreglo que se ha recorrido hasta ese momento. En la línea 53 se pone en

www.elsolucionario.net

23.11

Subprocesamiento múltiple con GUIs

979

inactividad el subproceso actual en ejecución durante un máximo de 4 milisegundos. En breve hablaremos sobre el por qué de esto. En la línea 61 se evalúa si el elemento del arreglo primos en el índice actual es true (y por ende, primo). De ser así, en la línea 63 se pasa el índice al método publish de manera que pueda mostrarse como resultado intermedio en la GUI, y en la línea 64 se incrementa el número de primos encontrados. En las líneas 66 y 67 se establecen en false todos los múltiplos del índice actual, para indicar que no son primos. Cuando se ha recorrido todo el arreglo boolean, el número de primos encontrados se devuelve en la línea 72. En las líneas 76 a 80 se declara el método process, el cual se ejecuta en el subproceso de despachamiento de eventos y recibe su argumento valsPublicados del método publish. El paso de valores entre publish en el subproceso trabajador y process en el subproceso de despachamiento de eventos es asíncrono; process no se invoca necesariamente para cada una de las llamadas a publish. Todos los objetos Integer publicados desde la última llamada a publish se reciben como un objeto List, a través del método process. En las líneas 78 y 79 se itera a través de esta lista y se muestran los valores publicados en un objeto JTextArea. Como el cálculo en el método doInBackground progresa rápidamente, publicando valores con frecuencia, las actualizaciones al objeto JTextArea se pueden apilar en el subproceso de despachamiento de eventos, lo que puede provocar que la GUI tenga una respuesta lenta. De hecho, al buscar un número suficientemente largo de primos, el subproceso de despachamiento de eventos puede llegar a recibir tantas peticiones en una rápida sucesión para actualizar el objeto JTextArea, que el subproceso se quedará sin memoria en su cola de eventos. Ésta es la razón por la cual pusimos al subproceso trabajador en inactividad durante unos cuantos milisegundos, entre cada llamada potencial a publish. Se reduce la velocidad del cálculo lo suficiente como para permitir que el subproceso despachador de eventos se mantenga a la par con las peticiones para actualizar el objeto JTextArea con nuevos números primos, lo cual permite a la GUI actualizarse de manera uniforme y así puede permanecer con una capacidad de respuesta relativamente inmediata. En las líneas 83 a 106 se define el método done. Cuando el cálculo termina o se cancela, el método done habilita el botón Obtener primos y deshabilita el botón Cancelar (líneas 85 y 86). En la línea 92 se obtiene el valor de retorno (el número de primos encontrados) del método doInBackground. En las líneas 94 a 103 se atrapan las excepciones lanzadas por el método get y se muestra un mensaje de error apropiado en el objeto estadoJLabel. Si no ocurren excepciones, en la línea 105 se establece el objeto estadoJLabel para indicar el número de primos encontrados. En las líneas 109 a 112 se define el método public detenerCalculo, el cual se invoca cuando el usuario hace clic en el botón Cancelar. Este método establece la bandera detenido en la línea 111, de forma que doInBackground pueda regresar sin terminar su cálculo la próxima vez que evalúe esta bandera. Aunque SwingWorker cuenta con un método cancel, este método simplemente llama al método interrupt de Thread en el subproceso trabajador. Al utilizar la bandera boolean en vez del método cancel, podemos detener el cálculo limpiamente, devolver un valor de doInBackground y asegurar que se haga una llamada al método done, aun si el cálculo no se ejecutó hasta completarse, sin el riesgo de lanzar una excepción InterruptedException asociada con la acción de interrumpir el subproceso trabajador. La clase BuscarPrimos (figura 23.28) muestra un objeto JTextField que permite al usuario escribir un número, un objeto JButton para empezar a buscar todos los números primos menores que ese número, y un objeto JTextArea para mostrar los números primos. Un objeto JButton permite al usuario cancelar el cálculo, y un objeto JProgressBar indica el progreso del cálculo. El constructor de BuscarPrimos (líneas 32 a 125) inicializa estos componentes y los muestra en un objeto JFrame, usando el esquema BorderLayout. En las líneas 42 a 94 se registra el manejador de eventos para el objeto obtenerPrimosJButton. Cuando el usuario hace clic en este objeto JButton, en las líneas 47 a 49 se restablece el objeto JProgressBar y se borran los objetos mostrarPrimosJTextArea y estadoJLabel. En las líneas 53 a 63 se analiza el valor en el objeto JtextField y se muestra un mensaje de error si el valor no es un entero. En las líneas 66 a 68 se construye un nuevo objeto CalculadoraPrimos, el cual recibe como argumentos el entero que escribió el usuario, el objeto mostrarPrimosJTextArea para mostrar los números primos, el objeto estadoJLabel y los dos objetos JButton. En las líneas 71 a 85 se registra un objeto PropertyChangeListener para el nuevo objeto CalculadoraPrimos, mediante el uso de una clase interna anónima. PropertyChangeListener es una interfaz del paquete java.beans que define un solo método, propertyChange. Cada vez que se invoca el método setProgress en un objeto CalculadoraPrimos, este objeto genera un evento PropertyChangeEvent para indicar que la propiedad de progreso ha cambiado. El método propertyChange escucha estos eventos. En la línea 78 se evalúa si un evento PropertyChangeEvent dado indica un cambio en la propiedad de progreso. De ser así, en la línea 80 se obtiene el nuevo valor de la propiedad y en la línea 81 se actualiza el objeto JProgressBar con el nuevo valor

www.elsolucionario.net

980

Capítulo 23

Subprocesamiento múltiple

de la propiedad de progreso. El objeto JButton “Obtener primos” se deshabilita (línea 88), de manera que sólo se pueda ejecutar un cálculo que actualice la GUI a la vez, y el objeto JButton “Cancelar” se habilita (línea 89) para permitir que el usuario detenga el cálculo antes de completarse. En la línea 91 se ejecuta el objeto CalculadoraPrimos para empezar a buscar números primos. Si el usuario hace clic en el objeto cancelarJButton, el manejador de eventos registrado en las líneas 107 a 115 llama al método detenerCalculo de CalculadoraPrimos (línea 112) y el cálculo regresa antes de terminar.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49

// Fig 23.28: BuscarPrimos.java // Uso de un objeto SwingWorker para mostrar números primos y actualizar un objeto // JProgressBar mientras se calculan los números primos. import javax.swing.JFrame; import javax.swing.JTextField; import javax.swing.JTextArea; import javax.swing.JButton; import javax.swing.JProgressBar; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.ScrollPaneConstants; import java.awt.BorderLayout; import java.awt.GridLayout; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import java.util.concurrent.ExecutionException; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeEvent; public class BuscarPrimos extends JFrame { private final JTextField primoMayor = new JTextField(); private final JButton obtenerPrimosJButton = new JButton( "Obtener primos" ); private final JTextArea mostrarPrimosJTextArea = new JTextArea(); private final JButton cancelarJButton = new JButton( "Cancelar" ); private final JProgressBar progresoJProgressBar = new JProgressBar(); private final JLabel estadoJLabel = new JLabel(); private CalculadoraPrimos calculadora; // constructor public BuscarPrimos() { super( "Busqueda de primos con SwingWorker" ); setLayout( new BorderLayout() ); // inicializa el panel para obtener un número del usuario JPanel norteJPanel = new JPanel(); norteJPanel.add( new JLabel( "Buscar primos menores que: " ) ); primoMayor.setColumns( 5 ); norteJPanel.add( primoMayor ); obtenerPrimosJButton.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e ) { progresoJProgressBar.setValue( 0 ); // restablece JProgressBar mostrarPrimosJTextArea.setText( "" ); // borra JTextArea estadoJLabel.setText( "" ); // borra JLabel

Figura 23.28 | Uso de un objeto SwingWorker para mostrar números primos y actualizar un objeto JProgressBar mientras se calculan los números primos. (Parte 1 de 3).

www.elsolucionario.net

23.11

50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107

Subprocesamiento múltiple con GUIs

981

int numero; try { // obtiene la entrada del usuario numero = Integer.parseInt( primoMayor.getText() ); } // fin de try catch ( NumberFormatException ex ) { estadoJLabel.setText( "Escriba un entero." ); return; } // fin de catch // construye un nuevo objeto CalculadoraPrimos calculadora = new CalculadoraPrimos( numero, mostrarPrimosJTextArea, estadoJLabel, obtenerPrimosJButton, cancelarJButton ); // escucha en espera de cambios en la propiedad de la barra de progreso calculadora.addPropertyChangeListener( new PropertyChangeListener() { public void propertyChange( PropertyChangeEvent e ) { // si la propiedad modificada es progreso (progress), // actualiza la barra de progreso if ( e.getPropertyName().equals( "progress" ) ) { int nuevoValor = ( Integer ) e.getNewValue(); progresoJProgressBar.setValue( nuevoValor ); } // fin de if } // fin del método propertyChange } // fin de la clase interna anónima ); // fin de la llamada a addPropertyChangeListener // deshabilita el botón Obtener primos y habilita el botón Cancelar obtenerPrimosJButton.setEnabled( false ); cancelarJButton.setEnabled( true ); calculadora.execute(); // ejecuta el objeto CalculadoraPrimos } // fin del método ActionPerformed } // fin de la clase interna anónima ); // fin de la llamada a addActionListener norteJPanel.add( obtenerPrimosJButton ); // agrega un objeto JList desplazable para mostrar el resultado del cálculo mostrarPrimosJTextArea.setEditable( false ); add( new JScrollPane( mostrarPrimosJTextArea, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER ) ); // inicializa un panel para mostrar a cancelarJButton, // progresoJProgressBar y estadoJLabel JPanel surJPanel = new JPanel( new GridLayout( 1, 3, 10, 10 ) ); cancelarJButton.setEnabled( false ); cancelarJButton.addActionListener(

Figura 23.28 | Uso de un objeto SwingWorker para mostrar números primos y actualizar un objeto JProgressBar mientras se calculan los números primos. (Parte 2 de 3).

www.elsolucionario.net

982

108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133

Capítulo 23

Subprocesamiento múltiple

new ActionListener() { public void actionPerformed( ActionEvent e ) { calculadora.detenerCalculo(); // cancela el cálculo } // fin del método ActionPerformed } // fin de la clase interna anónima ); // fin de la llamada a addActionListener surJPanel.add( cancelarJButton ); progresoJProgressBar.setStringPainted( true ); surJPanel.add( progresoJProgressBar ); surJPanel.add( estadoJLabel ); add( norteJPanel, BorderLayout.NORTH ); add( surJPanel, BorderLayout.SOUTH ); setSize( 350, 300 ); setVisible( true ); } // fin del constructor // el método main empieza la ejecución del programa public static void main( String[] args ) { BuscarPrimos aplicacion = new BuscarPrimos(); aplicacion.setDefaultCloseOperation( EXIT_ON_CLOSE ); } // fin de main } // fin de la clase BuscarPrimos a)

b)

c)

Figura 23.28 | Uso de un objeto SwingWorker para mostrar números primos y actualizar un objeto JProgressBar mientras se calculan los números primos. (Parte 3 de 3).

23.12 Otras clases e interfaces en java.util.concurrent

La interfaz Runnable sólo proporciona la funcionalidad más básica para la programación con subprocesamiento múltiple. De hecho, esta interfaz tiene varias limitaciones. Suponga que un objeto Runnable encuentra un problema y trata de lanzar una excepción verificada. El método run no se declara para lanzar ninguna excepción, por lo que el problema se debe manejar dentro del objeto Runnable; la excepción no se puede pasar al subproceso que hizo la llamada. Ahora, suponga que un objeto Runnable va a realizar un cálculo extenso, y que la aplicación desea obtener el resultado de ese cálculo. El método run no puede devolver un valor, por lo que la aplicación debe utilizar datos compartidos para pasar el valor de vuelta al subproceso que hizo la llamada. Esto también implica la sobrecarga de sincronizar el acceso a los datos. Los desarrolladores de las APIs de concurrencia que se introdujeron en Java SE 5 reconocieron estas limitaciones, y crearon una nueva interfaz para corregirlas. La interfaz Callable

www.elsolucionario.net

Resumen

983

(del paquete java.util.concurrent) declara un solo método llamado call. Esta interfaz está diseñada para que sea similar a la interfaz Runnable (permitir que se realice una acción de manera concurrente en un subproceso separado), pero el método call permite al subproceso devolver un valor o lanzar una excepción verificada. Es probable que una aplicación que crea un objeto Callable necesite ejecutar el objeto Callable de manera concurrente con otros objetos Runnable y Callable. La interfaz ExecutorService proporciona el método submit, el cual ejecuta un objeto Callable que recibe como argumento. El método submit devuelve un objeto de tipo Future (del paquete java.util.concurrent), la cual es una interfaz que representa al objeto Callable en ejecución. La interfaz Future declara el método get para devolver el resultado del objeto Callable y proporciona otros métodos para administrar la ejecución de un objeto Callable.

23.13 Conclusión En este capítulo aprendió que, a través de la historia, la concurrencia se ha implementado con las primitivas de sistema operativo, disponibles sólo para los programadores experimentados de sistemas, pero que Java pone la concurrencia a nuestra disposición a través del lenguaje y las APIs. También aprendió que la JVM en sí crea subprocesos para ejecutar un programa, y que también puede crear subprocesos para realizar las tareas de mantenimiento, como la recolección de basura. Hablamos sobre el ciclo de vida de un subproceso y los estados que éste puede ocupar durante su tiempo de vida. También hablamos sobre las prioridades de los subprocesos de Java, las cuales ayudan al sistema a programar los subprocesos para su ejecución. Aprendió que debe evitar manipular las prioridades de los subprocesos en Java directamente, y aprendió también acerca de los problemas asociados con las prioridades de los subprocesos, como el aplazamiento indefinido (conocido también como inanición). Después presentamos la interfaz Runnable, que se utiliza para especificar que una tarea se puede ejecutar de manera concurrente con otras tareas. El método run de esta interfaz se invoca mediante el subproceso que ejecuta la tarea. Mostramos cómo ejecutar un objeto Runnable, asociándolo con un objeto de la clase Thread. Después mostramos cómo usar la interfaz Executor para administrar la ejecución de objetos Runnable a través de reservas de subprocesos, las cuales pueden reutilizar los subprocesos existentes para eliminar la sobrecarga de tener que crear un nuevo subproceso para cada tarea, y pueden mejorar el rendimiento al optimizar el número de procesadores, para asegurar que el procesador se mantenga ocupado. Aprendió que cuando varios subprocesos comparten un objeto y uno o más de ellos modifica ese objeto, pueden ocurrir resultados indeterminados, a menos que el acceso al objeto compartido se administre de manera apropiada. Le mostramos cómo resolver este problema a través de la sincronización de subprocesos, la cual coordina el acceso a los datos compartidos por varios subprocesos concurrentes. Conoció varias técnicas para realizar la sincronización: primero con la clase integrada ArrayBlockingQueue (la cual se encarga de todos los detalles de sincronización por usted), después con los monitores integrados de Java y la palabra clave synchronized, y finalmente con las interfaces Lock y Condition. Hablamos sobre el hecho de que las GUIs de Swing no son seguras para los subprocesos, por lo que todas las interacciones con (y las modificaciones a) la GUI deben realizarse en el subproceso despachador de eventos. También vimos los problemas asociados con la realización de cálculos extensos en el subproceso despachador de eventos. Después le mostramos cómo puede usar la clase SwingWorker de Java SE 6 para realizar cálculos extensos en subprocesos trabajadores. Aprendió a mostrar los resultados de un objeto SwingWorker en una GUI cuando se completa el cálculo, y a mostrar los resultados intermedios cuando el cálculo se está realizando. Por último, hablamos sobre las interfaces Callable y Future, las cuales nos permiten ejecutar tareas que devuelvan resultados y a obtener esos resultados, respectivamente. En el capítulo 24, Redes, utilizaremos las técnicas de subprocesamiento múltiple que presentamos aquí para que nos ayuden a crear servidores con subprocesamiento múltiple, que puedan actuar con varios clientes de manera concurrente.

Resumen Sección 23.1 Introducción • A través de la historia, la concurrencia se ha implementado con primitivas de sistema operativo, disponibles sólo para los programadores de sistemas experimentados.

www.elsolucionario.net

984

Capítulo 23

Subprocesamiento múltiple

• El lenguaje de programación Ada, desarrollado por el Departamento de defensa de los Estados Unidos, hizo que las primitivas de concurrencia estuvieran disponibles ampliamente para los contratistas de defensa dedicados a la construcción de sistemas militares de comando y control. • Java pone las primitivas de concurrencia a disposición del programador de aplicaciones, a través del lenguaje y de las APIs. El programador especifica que una aplicación contiene subprocesos de ejecución separados, en donde cada subproceso tiene su propia pila de llamadas a métodos y su propio contador, lo cual le permite ejecutarse concurrentemente con otros subprocesos, al mismo tiempo que comparte los recursos a nivel de aplicación (como la memoria) con estos otros subprocesos. • Además de crear subprocesos para ejecutar un programa, la JVM también puede crear subprocesos para realizar tareas de mantenimiento, como la recolección de basura.

Sección 23.2 Estados de los subprocesos: ciclo de vida de un subproceso • En cualquier momento dado, se dice que un subproceso se encuentra en uno de varios estados de subproceso. • Un nuevo subproceso empieza su ciclo cuando en el estado nuevo. Permanece en este estado hasta que el programa inicia el subproceso, con lo cual se coloca en el estado ejecutable. Se considera que un subproceso en el estado ejecutable está ejecutando su tarea. • Algunas veces, un subproceso ejecutable cambia al estado en espera mientras espera a que otro subproceso realice una tarea. Un subproceso en espera regresa al estado ejecutable sólo cuando otro subproceso notifica al subproceso en espera que puede continuar ejecutándose. • Un subproceso ejecutable puede entrar al estado en espera sincronizado durante un intervalo específico de tiempo. Regresa al estado ejecutable cuando ese intervalo de tiempo expira, o cuando ocurre el evento que está esperando. • Un subproceso ejecutable puede cambiar el estado en espera sincronizado si proporciona un intervalo de espera opcional cuando está esperando a que otro subproceso realice una tarea. Dicho subproceso regresará al estado ejecutable cuando otro subproceso se lo notifique o cuando expire el intervalo sincronizado. • Un subproceso inactivo permanece en el estado en espera sincronizado durante un periodo designado de tiempo, después del cual regresa al estado ejecutable. • Un subproceso ejecutable cambia al estado bloqueado cuando trata de realizar una tarea que no puede completarse inmediatamente, y debe esperar temporalmente hasta que se complete esa tarea. Al estado bloqueado cuando trata de realizar una tarea que no puede completarse inmediatamente, y debe esperar temporalmente hasta que se complete esa tarea. En ese punto, el subproceso bloqueado cambia al estado ejecutable, para poder continuar su ejecución. Un subproceso bloqueado no puede usar un procesador, aun si hay uno disponible. • Un subproceso ejecutable entra al estado terminado cuando completa exitosamente su tarea, o termina de alguna otra forma (tal vez debido a un error). • A nivel del sistema operativo, el estado ejecutable de Java generalmente abarca dos estados separados. Cuando un subproceso cambia por primera vez al estado ejecutable desde el estado nuevo, el subproceso se encuentra en el estado listo. Un subproceso listo entra al estado en ejecución cuando el sistema operativo lo asigna a un procesador; a esto también se le conoce como despachar el subproceso. • En la mayoría de los sistemas operativos, a cada subproceso se le otorga una pequeña cantidad de tiempo del procesador (lo cual se conoce como quantum o intervalo de tiempo) en la que debe realizar su tarea. Cuando expira su quantum, el subproceso regresa al estado listo y el sistema operativo asigna otro subproceso al procesador. • El proceso que utiliza un sistema operativo para determinar qué subproceso debe despachar se conoce como programación de subprocesos, y depende de las prioridades de los subprocesos.

Sección 23.3 Prioridades y programación de subprocesos • Todo subproceso en Java tiene una prioridad de subproceso (de MIN_PRIORITY a MAX_PRIORITY), la cual ayuda al sistema operativo a determinar el orden en el que se programan los subprocesos. • De manera predeterminada, cada subproceso recibe la prioridad NORM_PRIORITY (una constante de 5). Cada nuevo subproceso hereda la prioridad del subproceso que lo creó. • La mayoría de los sistemas operativos permiten que los subprocesos con igual prioridad compartan un procesador con intervalo de tiempo. • El trabajo del programador de subprocesos de un sistema operativo es determinar cuál subproceso se debe ejecutar a continuación. • Cuando un subproceso de mayor prioridad entra al estado listo, el sistema operativo generalmente sustituye el subproceso actual en ejecución para dar preferencia al subproceso de mayor prioridad (una operación conocida como programación preferente).

www.elsolucionario.net

Resumen

985

• Dependiendo del sistema operativo, los subprocesos de mayor prioridad podrían posponer (tal vez de manera indefinida) la ejecución de los subprocesos de menor prioridad. Comúnmente, a dicho aplazamiento indefinido se le conoce, en forma más colorida, como inanición.

Sección 23.4 Creación y ejecución de subprocesos • El medio preferido de crear aplicaciones en Java con subprocesamiento múltiple es mediante la implementación de la interfaz Runnable (del paquete java.lang). Un objeto Runnable representa una “tarea” que puede ejecutarse concurrentemente con otras tareas. • La interfaz Runnable declara el método run, en el cual podemos colocar el código que define la tarea a realizar. El subproceso que ejecuta un objeto Runnable llama al método run para realizar la tarea. • Un programa no terminará sino hasta que su último subproceso termine de ejecutarse; en este punto, la JVM también terminará. • No podemos predecir el orden en el que se van a programar los subprocesos, aun si conocemos el orden en el que se crearon y se iniciaron. • Aunque es posible crear subprocesos en forma explícita, se recomienda utilizar la interfaz Executor para administrar la ejecución de objetos Runnable de manera automática. Por lo general, un objeto Executor crea y administra un grupo de subprocesos, al cual se le denomina reserva de subprocesos, para ejecutar objetos Runnable. • Los objetos Executor pueden reutilizar los subprocesos existentes para eliminar la sobrecarga de crear un nuevo subproceso para cada tarea, y pueden mejorar el rendimiento al optimizar el número de subprocesos, con lo cual se asegura que el procesador se mantenga ocupado. • La interfaz Executor declara un solo método llamado execute, el cual acepta un objeto Runnable como argumento y lo asigna a uno de los subprocesos disponibles en la reserva. Si no hay subprocesos disponibles, el objeto Executor crea un nuevo subproceso, o espera a que haya uno disponible. • La interfaz ExecutorService (del paquete java.util.concurrent) extiende a la interfaz Executor y declara varios métodos más para administrar el ciclo de vida de un objeto Executor. • Un objeto que implementa a la interfaz ExecutorService se puede crear mediante el uso de los métodos static declarados en la clase Executors (del paquete java.util.concurrent). • El método newCachedThreadPool de Executors devuelve un objeto ExecutorService que crea nuevos subprocesos, según los va necesitando la aplicación. • El método execute de ExecutorService ejecuta el objeto Runnable que recibe como argumento en algún momento en el futuro. El método regresa inmediatamente después de cada invocación; el programa no espera a que termine cada tarea. • El método shutdown de ExecutorService notifica al objeto ExecutorService para que deje de aceptar nuevas tareas, pero continúa ejecutando las tareas que ya se hayan enviado. Una vez que se han completado todos los objetos Runnable enviados anteriormente, el objeto ExecutorService termina.

Sección 23.5 Sincronización de subprocesos • Cuando varios subprocesos comparten un objeto, y éste puede ser modificado por uno o más de los subprocesos, pueden ocurrir resultados indeterminados a menos que el acceso al objeto compartido se administre de manera apropiada. El problema puede resolverse si se da a un subproceso a la vez el acceso exclusivo al código que manipula al objeto compartido. Durante ese tiempo, otros subprocesos que deseen manipular el objeto deben mantenerse en espera. Cuando el subproceso con acceso exclusivo al objeto termina de manipularlo, a uno de los subprocesos que estaba en espera se le debe permitir que continúe ejecutándose. Este proceso, conocido como sincronización de subprocesos, coordina el acceso a los datos compartidos por varios subprocesos concurrentes. • Al sincronizar los subprocesos, podemos asegurar que cada subproceso que accede a un objeto compartido excluye a los demás subprocesos de hacerlo en forma simultánea; a esto se le conoce como exclusión mutua. • Una manera común de realizar la sincronización es mediante los monitores integrados en Java. Cada objeto tiene un monitor y un bloqueo de monitor. El monitor asegura que el bloqueo de monitor de su objeto se mantenga por un máximo de sólo un subproceso a la vez y, por ende, se puede utilizar para imponer la exclusión mutua. • Si una operación requiere que el subproceso en ejecución mantenga un bloqueo mientras se realiza la operación, un subproceso debe adquirir el bloqueo para poder continuar con la operación. Otros subprocesos que traten de realizar una operación que requiera el mismo bloqueo permanecerán bloqueados hasta que el primer subproceso libere el bloqueo, punto en el cual los subprocesos bloqueados pueden tratar de adquirir el bloqueo. • Para especificar que un subproceso debe mantener un bloqueo de monitor para ejecutar un bloque de código, el código debe colocarse en una instrucción synchronized. Se dice que dicho código está protegido por el bloqueo de monitor; un subproceso debe adquirir el bloqueo para ejecutar las instrucciones synchronized.

www.elsolucionario.net

986

Capítulo 23

Subprocesamiento múltiple

• Las instrucciones synchronized se declaran mediante la palabra clave synchronized: synchronized ( {

objeto

)

instrucciones

} // fin de la instrucción synchronized

• •

• •

en donde objeto es el objeto cuyo bloqueo de monitor se va a adquirir; generalmente, objeto es this si es el objeto en el que aparece la instrucción synchronized. Un método synchronized es equivalente a una instrucción synchronized que encierra el cuerpo completo de un método, y que utiliza a this como el objeto cuyo bloqueo de monitor se va a adquirir. La interfaz ExecutorService proporciona el método awaitTermination para obligar a un programa a esperar a que los subprocesos completen su ejecución. Este método devuelve el control al que lo llamó, ya sea cuando se completen todas las tareas que se ejecutan en el objeto ExecutorService, o cuando se agote el tiempo de inactividad especificado. Si todas las tareas se completan antes de que se agote el tiempo de awaitTermination, este método devuelve true; en caso contrario devuelve false. Los dos argumentos para awaitTermination representan un valor de límite de tiempo y una unidad de medida especificada con una constante de la clase TimeUnit. Podemos simular la atomicidad al asegurar que sólo un subproceso lleve a cabo un conjunto de operaciones al mismo tiempo. La atomicidad se puede lograr mediante el uso de la palabra clave synchronized para crear una instrucción o método synchronized. Al compartir datos inmutables entre subprocesos, debemos declarar los campos de datos correspondientes como final, para indicar que los valores de las variables no cambiarán una vez que se inicialicen.

Sección 23.6 Relación productor/consumidor sin sincronización • En una relación productor/consumidor con subprocesamiento múltiple, un subproceso productor genera los datos y los coloca en un objeto compartido, llamado búfer. Un subproceso consumidor lee los datos del búfer. • Las operaciones con los datos del búfer compartidos por un subproceso productor y un subproceso consumidor son dependientes del estado; las operaciones deben proceder sólo si el búfer se encuentra en el estado correcto. Si el búfer se encuentra en un estado en el que no esté completamente lleno, el productor puede producir; si el búfer se encuentra en un estado en el que no esté completamente vacío, el consumidor puede consumir. • Los subprocesos con acceso a un búfer deben sincronizarse para asegurar que lo datos se escriban en el búfer, o se lean del búfer sólo si éste se encuentra en el estado apropiado. Si el productor que trata de colocar los siguientes datos en el búfer determina que éste se encuentra lleno, el subproceso productor debe esperar hasta que haya espacio. Si un subproceso consumidor encuentra el búfer vacío o que los datos anteriores ya se han leído, debe también esperar hasta que haya nuevos datos disponibles.

Sección 23.7 Relación productor/consumidor: ArrayBlockingQueue • Java incluye una clase de búfer completamente implementada llamada ArrayBlockingQueue en el paquete java. util.concurrent, que implementa a la interfaz BlockingQueue. Esta interfaz extiende a la interfaz Queue y declara los métodos put y take, los equivalentes con bloqueo de los métodos offer y poll de Queue, respectivamente. • El método put coloca un elemento al final del objeto BlockingQueue, y espera si la cola está llena. El método take elimina un elemento de la parte inicial del objeto BlockingQueue, y espera si la cola está vacía. Estos métodos hacen que la clase ArrayBlockingQueue sea una buena opción para implementar un búfer compartido. Debido a que el método put bloquea hasta que haya espacio en el búfer para escribir datos, y el método take bloquea hasta que haya nuevos datos para leer, el productor debe producir primero un valor, el consumidor sólo consume correctamente hasta después de que el productor escribe un valor, y el productor produce correctamente el siguiente valor (después del primero) sólo hasta que el consumidor lea el valor anterior (o primero). • ArrayBlockingQueue almacena los datos compartidos en un arreglo. El tamaño de este arreglo se especifica como argumento para el constructor de ArrayBlockingQueue. Una vez creado, un objeto ArrayBlockingQueue tiene su tamaño fijo y no se expandirá para dar cabida a más elementos.

Sección 23.8 Relación productor/consumidor con sincronización • Usted puede implementar su propio búfer compartido, usando la palabra clave synchronized y los métodos wait, notify y notifyAll de Object, que pueden usarse con condiciones para hacer que los subprocesos esperen cuando no puedan realizar sus tareas. • Si un subproceso obtiene el bloqueo de monitor en un objeto, y después determina que no puede continuar con su tarea en ese objeto sino hasta que se cumpla cierta condición, el subproceso puede llamar al método wait de

www.elsolucionario.net

Resumen

987

Object; esto libera el bloqueo de monitor en el objeto, y el subproceso queda en el estado en espera mientras el otro subproceso trata de entrar a la(s) instrucción(es) o método(s) synchronized del objeto. • Cuando un subproceso que ejecuta una instrucción (o método) synchronized completa o cumple con la condición en la que otro subproceso puede estar esperando, puede llamar al método notify de Object para permitir que un subproceso en espera cambie al estado ejecutable de nuevo. En este punto, el subproceso que cambió del estado en espera al estado ejecutable puede tratar de readquirir el bloqueo de monitor en el objeto. • Si un subproceso llama a notifyAll, entonces todos los subprocesos que esperan el bloqueo de monitor se convierten en candidatos para readquirir el bloqueo (es decir, todos cambian al estado ejecutable).

Sección 23.9 Relación productor/consumidor: búferes delimitados • No podemos hacer suposiciones acerca de las velocidades relativas de los subprocesos concurrentes; las interacciones que ocurren con el sistema operativo, la red, el usuario y otros componentes pueden hacer que los subprocesos operen a distintas velocidades. Cuando esto ocurre, los subprocesos esperan. • Para minimizar la cantidad de tiempo de espera para los subprocesos que comparten recursos y operan a las mismas velocidades promedio, podemos implementar un bufer delimitado. Si el productor produce temporalmente valores con más rapidez de la que el consumidor pueda consumirlos, el productor puede escribir otros valores en el espacio adicional del búfer (si hay disponible). Si el consumidor consume con más rapidez de la que el productor produce nuevos valores, el consumidor puede leer valores adicionales (si los hay) del búfer. • La clave para usar un búfer delimitado con un productor y un consumidor que operan aproximadamente a la misma velocidad es proporcionar al búfer suficientes ubicaciones para que pueda manejar la producción “extra” anticipada. • La manera más simple de implementar un búfer delimitado es utilizar un objeto ArrayBlockingQueue para el búfer, de manera que se haga cargo de todos los detalles de la sincronización por nosotros.

Sección 23.10 Relación productor/consumidor: las interfaces Lock y Condition • Las interfaces Lock y Condition, que se introdujeron en Java SE 5, proporcionan a los programadores un control más preciso sobre la sincronización de los subprocesos, pero son más complicadas de usar. • Cualquier objeto puede contener una referencia a un objeto que implemente a la interfaz Lock (del paquete java. util.concurrent.locks). Un subproceso llama al método lock de Lock para adquirir el bloqueo. Una vez que un subproceso obtiene un objeto Lock, este objeto no permitirá que otro subproceso obtenga el Lock sino hasta que el primer subproceso lo libere (llamando al método unlock de Lock). • Si varios subprocesos tratan de llamar al método lock en el mismo objeto Lock y al mismo tiempo, sólo uno de estos subprocesos puede obtener el bloqueo; todos los demás se colocan en el estado en espera de ese bloqueo. Cuando un subproceso llama al método unlock, se libera el bloqueo sobre el objeto y un subproceso en espera que intente bloquear el objeto puede continuar. • La clase ReentrantLock (del paquete java.util.concurrent.locks) es una implementación básica de la interfaz Lock. • El constructor de ReentrantLock recibe un argumento boolean, el cual especifica si el bloqueo tiene una política de equidad. Si el argumento es true, la política de equidad de ReentrantLock es: “el subproceso con más tiempo de espera adquirirá el bloqueo cuando esté disponible”. Dicha política de equidad garantiza que nunca ocurra el aplazamiento indefinido (también conocido como inanición). Si el argumento de la política de equidad se establece en false, no hay garantía en cuanto a cuál subproceso en espera adquirirá el bloqueo cuando esté disponible. • Si un subproceso que posee un objeto Lock determina que no puede continuar con su tarea hasta que se cumpla cierta condición, el subproceso puede esperar en base a un objeto de condición. El uso de objetos Lock nos permite declarar de manera explícita los objetos de condición sobre los cuales un subproceso tal vez tenga que esperar. • Los objetos de condición se asocian con un objeto Lock específico y se crean mediante una llamada al método newCondition de Lock, el cual devuelve un objeto que implementa a la interfaz Condition (del paquete java. util.concurrent.locks). Para esperar en base a un objeto de condición, el subproceso puede llamar al método await de Condition. Esto libera de inmediato el objeto Lock asociado, y coloca al subproceso en el estado en espera, en base a ese objeto Condition. Así, otros subprocesos pueden tratar de obtener el objeto Lock. • Cuando un subproceso ejecutable completa una tarea y determina que el subproceso en espera puede ahora continuar, el subproceso ejecutable puede llamar al método signal de Condition para permitir que un subproceso en el estado en espera de ese objeto Condition regrese al estado ejecutable. En este punto, el subproceso que cambió del estado en espera al estado ejecutable puede tratar de readquirir el objeto Lock. • Si hay varios subprocesos en un estado en espera de un objeto Condition cuando se hace la llamada a signal, la implementación predeterminada de Condition indica al subproceso con más tiempo de espera que debe cambiar al estado ejecutable.

www.elsolucionario.net

988

Capítulo 23

Subprocesamiento múltiple

• Si un subproceso llama al método signalAll de Condition, entonces todos los subprocesos que esperan esa condición cambian al estado ejecutable y se convierten en candidatos para readquirir el objeto Lock. • Cuando un subproceso termina con un objeto compartido, debe llamar al método unlock para liberar al objeto Lock. • En algunas aplicaciones, el uso de objetos Lock y Condition puede ser preferible a utilizar la palabra clave synchronized. Los objetos Lock nos permiten interrumpir a los subprocesos en espera, o especificar un tiempo límite para esperar a adquirir un bloqueo, lo cual no es posible si se utiliza la palabra clave synchronized. Además, un objeto Lock no está restringido a ser adquirido y liberado en el mismo bloque de código, lo cual es el caso con la palabra clave synchronized. • Los objetos Condition nos permiten especificar varios objetos de condición, en base a los cuales los subprocesos pueden esperar. Por ende, es posible indicar a los subprocesos en espera que un objeto de condición específico es ahora verdadero, llamando a signal o signalAll en ese objeto Condition. Con la palabra clave synchronized, no hay forma de indicar de manera explícita la condición en la cual esperan los subprocesos y, por lo tanto, no hay forma de notificar a los subprocesos en espera de una condición específica que pueden continuar, sin también indicarlo a los subprocesos que están en espera de otras condiciones.

Sección 23.11 Subprocesamiento múltiple con GUIs • Las aplicaciones de Swing tienen un subproceso, conocido como el subproceso de despachamiento de eventos, para manejar las interacciones con los componentes de la GUI de la aplicación. Todas las tareas que requieren interacción con la GUI de una aplicación se colocan en una cola de eventos y se ejecutan en forma secuencial, mediante el subproceso de despachamiento de eventos. • Los componentes de GUI de Swing no son seguros para los subprocesos. La seguridad de subprocesos en aplicaciones de GUI se logra asegurando que se acceda a los componentes de Swing sólo desde el subproceso de despachamiento de eventos. A esta técnica se le conoce como confinamiento de subprocesos. • Si una aplicación debe realizar un cálculo extenso en respuesta a una interacción con la interfaz del usuario, el subproceso de despachamiento de eventos no puede atender otras tareas en la cola de eventos, mientras se encuentre atado en ese cálculo. Esto hace que los componentes de la GUI pierdan su capacidad de respuesta. Es preferible manejar un cálculo extenso en un subproceso separado, con lo cual el subproceso de despachamiento de eventos queda libre para continuar administrando las demás interacciones con la GUI. • Java SE 6 cuenta con la clase SwingWorker (en el paquete javax.swing), que implementa a la interfaz Runnable, para realizar cálculos extensos en un subproceso trabajador, y para actualizar los componentes de Swing desde el subproceso de despachamiento de eventos, con base en los resultados del cálculo. • Para utilizar las herramientas de SwingWorker, cree una clase que extienda a SwingWorker y sobrescriba los métodos doInBackground y done. El método doInBackground realiza el cálculo y devuelve el resultado. El método done muestra los resultados en la GUI. • SwingWorker es una clase genérica. Su primer parámetro de tipo indica el tipo devuelto por el método doInBackground; el segundo indica el tipo que se pasa entre los métodos publish y process para manejar los resultados intermedios. • El método doInBackground se llama desde un subproceso trabajador. Después de que doInBackground regresa, el método done se llama desde el subproceso de despachamiento de eventos para mostrar los resultados. • Una excepción ExecutionException se lanza si ocurre una excepción durante el cálculo. • SwingWorker también cuenta con los métodos publish, process y setProgress. El método publish envía en forma repetida los resultados inmediatos al método process, el cual muestra los resultados en un componente de la GUI. El método setProgress actualiza la propiedad de progreso. • El método progress se ejecuta en el subproceso de despachamiento de eventos y recibe datos del método publish. El paso de valores entre publish en el subproceso trabajador y process en el subproceso de despachamiento de eventos es asíncrono; process no se invoca necesariamente para cada llamada a publish. • PropertyChangeListener es una interfaz del paquete java.beans que define un solo método, propertyChange. Cada vez que se invoca el método setProgress, se genera un evento PropertyChangedEvent para indicar que la propiedad de progreso ha cambiado.

Sección 23.12 Otras clases e interfaces en java.util.concurrent • La interfaz Callable (del paquete java.util.concurrent) declara un solo método llamado call. Esta interfaz está diseñada para que sea similar a la interfaz Runnable (permitir que se realice una acción de manera concurrente en un subproceso separado), pero el método call permite al subproceso devolver un valor o lanzar una excepción verificada.

www.elsolucionario.net

Terminología

989

• Es probable que una aplicación que crea un objeto Callable necesite ejecutar el objeto Callable de manera concurrente con otros objetos Runnable y Callable. La interfaz ExecutorService proporciona el método submit, el cual ejecuta un objeto Callable que recibe como argumento. • El método submit devuelve un objeto de tipo Future (del paquete java.util.concurrent), que representa al objeto Callable en ejecución. La interfaz Future declara el método get para devolver el resultado del objeto Callable y proporciona otros métodos para administrar la ejecución de un objeto Callable.

Terminología adquirir el bloqueo aplazamiento indefinido ArrayBlockingQueue, clase await, método de la interfaz Condition awaitTermination, método de la interfaz ExecutorService BlockingQueue,

interfaz bloqueado, estado bloqueo de monitor bloqueo intrínseco búfer búfer circular búfer delimitado call, método de la interfaz Callable Callable, interfaz cola de prioridad multinivel concurrencia Condition, interfaz confinamiento de subprocesos consumidor datos mutables dependencia de estados despachamiento de un subproceso ejecutable, estado en ejecución, estado en espera sincronizado, estado en espera, estado estado de un subproceso exclusión mutua execute, método de la interfaz Executor Executor, interfaz Executors, clase ExecutorService, interfaz Future, interfaz get, método de la interfaz Future IllegalMonitorStateException, clase inanición interbloqueo interrupt, método de la clase Thread InterruptedException, clase intervalo de inactividad intervalo de tiempo java.util.concurrent, paquete java.util.concurrent.locks, paquete listo, estado Lock, interfaz lock, método de la interfaz Lock monitor

newCachedThreadPool, método de la clase Executors newCondition, método de la interfaz Lock notify, método de la clase Object notifyAll, método de la clase Object

nuevo, estado objeto de condición operación atómica operaciones en paralelo política de equidad de un bloqueo prioridad de subprocesos productor programación cíclica (round-robin) programación concurrente programación de subprocesos programación preferente programador de subprocesos propertyChange, método de la interfaz PropertyChangeListener PropertyChangeListener,

interfaz protegido por un bloqueo put, método de la interfaz BlockingQueue quantum recolección de basura ReentrantLock, clase relación productor/consumidor reserva de subprocesos run, método de la interfaz Runnable Runnable, interfaz seguro para los subprocesos shutdown, método de la clase ExecutorService signal, método de la clase Condition signalAll, método de la clase Condition sincronización sincronización de subprocesos size, método de la clase ArrayBlockingQueue sleep, método de la clase Thread submit, método de la clase ExecutorService subprocesamiento múltiple subproceso subproceso consumidor subproceso de despachamiento de eventos subproceso inactivo subproceso principal subproceso productor SwingWorker, clase synchronized, instrucción synchronized, método synchronized, palabra clave

www.elsolucionario.net

990

Capítulo 23

Subprocesamiento múltiple

take,

método de la interfaz BlockingQueue terminado, estado Thread, clase

unlock,

método de la interfaz Lock valor pasado wait, método de la clase Object

Ejercicios de autoevaluación 23.1

Complete las siguientes oraciones: a) C y C++ son lenguajes con subprocesamiento _______________, mientras que Java es un lenguaje con subprocesamiento _______________. b) Un subproceso entra al estado terminado cuando _______________. c) Para detenerse durante cierto número designado de milisegundos y reanudar su ejecución, un subproceso debe llamar al método _______________ de la clase _______________. d) El método _______________ de la clase Condition pasa a un solo subproceso en el estado en espera de un objeto, al estado ejecutable. e) El método _______________ de la clase Condition pasa a todos los subprocesos en el estado en espera de un objeto, al estado ejecutable. f ) Un subproceso _______________ entra al estado _______________ cuando completa su tarea, o cuando termina de alguna otra forma. g) Un subproceso ejecutable puede entrar al estado _______________ durante un intervalo específico. h) A nivel del sistema operativo, el estado ejecutable en realidad abarca dos estados separados, ____________ ______ y _______________. i) Los objetos Runnable se ejecutan usando una clase que implementa a la interfaz _______________. j) El método _______________ de ExecutorService termina cada subproceso en un objeto ExecutorService tan pronto como termina de ejecutar su objeto Runnable actual, si lo hay. k) Un subproceso puede llamar al método _______________ en un objeto Condition para liberar el objeto Lock asociado, y colocar ese subproceso en el estado _______________. l) En una relación ___________, la porción correspondiente al _______________ de una aplicación genera datos y los almacena en un objeto compartido, y la porción correspondiente al _______________ de una aplicación lee datos del objeto compartido. m) La clase _______________ implementa a la interfaz BlockingQueue, usando un arreglo. n) La palabra clave _______________ indica que sólo se debe ejecutar un subproceso a la vez en un objeto.

23.2

Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué. a) Un subproceso no es ejecutable si ha terminado. b) Un subproceso ejecutable de mayor prioridad tiene preferencia sobre los subprocesos de menor prioridad. c) Algunos sistemas operativos utilizan el intervalo de tiempo con subprocesos. Por lo tanto, pueden permitir que los subprocesos desplacen a otros subprocesos de la misma prioridad. d) Cuando expira el quantum de un subproceso, éste regresa al estado ejecutable, a medida que el sistema operativo asigna el subproceso a un procesador. e) En un sistema con un solo procesador sin intervalo de tiempo, cada subproceso en un conjunto de subprocesos de igual prioridad (sin otros subprocesos presentes) se ejecuta hasta terminar, antes de que otros subprocesos de igual prioridad tengan oportunidad de ejecutarse.

Respuestas a los ejercicios de autoevaluación 23.1 a) simple, múltiple. b) termina su método run. c) sleep, Thread. d) signal. e) signalAll, f ) ejecutable, terminado. g) en espera sincronizado. h) listo, en ejecución. i) Executor. j) shutdown. k) await, en espera. l) productor/consumidor, productor, consumidor. m) ArrayBlockingQueue. n) synchronized. 23.2 a) Verdadero. b) Verdadero. c) Falso. El intervalo de tiempo permite a un subproceso ejecutarse hasta que expira su porción de tiempo (o quantum). Después pueden ejecutarse otros subprocesos de igual prioridad. d) Falso. Cuando expira el quantum de un subproceso, éste regresa al estado listo y el sistema operativo asigna otro subproceso al procesador. e) Verdadero.

www.elsolucionario.net

Ejercicios

991

Ejercicios 23.3

Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué. a) El método sleep no consume tiempo del procesador cuando un subproceso está inactivo. b) Al declarar un método como synchronized se garantiza que no pueda ocurrir el interbloqueo. c) Una vez que un subproceso obtiene un objeto Lock, éste no permitirá que otro subproceso obtenga el bloqueo sino hasta que el primer subproceso lo libere. d) Los componentes de Swing son seguros para los subprocesos.

23.4

Defina cada uno de los siguientes términos. a) subproceso b) subprocesamiento múltiple c) estado ejecutable d) estado en espera sincronizado e) programación preferente f ) interfaz Runnable g) método notifyAll h) relación productor/consumidor i) quantum

23.5

Describa cada uno de los siguientes términos en el contexto de los mecanismos de subprocesamiento en Java: a) synchronized b) productor c) consumidor d) wait e) notify f ) Lock g) Condition

23.6 Enliste las razones para entrar al estado bloqueado. Para cada una de éstas, describa la forma en que el programa comúnmente sale del estado bloqueado y entra al estado ejecutable. 23.7 Dos problemas que pueden ocurrir en sistemas que permiten a los subprocesos esperar son: el interbloqueo, en el cual uno o más subprocesos esperarán para siempre un evento que no puede ocurrir, y el aplazamiento indefinido, en donde uno o más subprocesos se retrasarán durante cierto tiempo, sin saber cuánto. Dé un ejemplo de cómo cada uno de estos problemas puede ocurrir en los programas de Java con subprocesamiento múltiple. 23.8 Escriba un programa para rebotar una pelota azul dentro de un objeto JPanel. La pelota deberá empezar a moverse con un evento mousePressed. Cuando la pelota pegue en el borde del objeto JPanel, deberá rebotar y continuar en la dirección opuesta. La pelota debe actualizarse mediante el uso de un objeto Runnable. 23.9 Modifique el programa del ejercicio 23.8 para agregar una nueva pelota cada vez que el usuario haga clic con el ratón. Proporcione un mínimo de 20 pelotas. Seleccione al azar el color para cada nueva pelota. 23.10 Modifique el programa del ejercicio 23.9 para agregar sombras. A medida que se mueva una pelota, dibuje un óvalo relleno de color negro en la parte inferior del objeto JPanel. Tal vez sería conveniente agregar un efecto tridimensional, incrementando o decrementando el tamaño de cada pelota cuando ésta pegue en el borde del subprograma.

www.elsolucionario.net

24 Redes Si la presencia de electricidad puede hacerse visible en cualquier parte de un circuito, no veo la razón por la cual la inteligencia no pueda transmitirse instantáneamente mediante la electricidad.

OBJETIVOS

—Samuel F. B. Morse

En este capítulo aprenderá a: Q

Comprender el trabajo en red en Java con URLs, sockets y datagramas.

Q

Implementar aplicaciones de red en Java, mediante el uso de sockets y datagramas.

Q

Implementar clientes y servidores en Java, que se comuniquen entre sí.

Q

Saber cómo implementar aplicaciones de colaboración basada en red.

Q

Construir un servidor con subprocesamiento múltiple.

El protocolo es todo. —Francois Giuliani

Lo que las redes de vías de ferrocarril, carreteras y canales fueron en otra época, las redes de telecomunicaciones, información y computación lo son hoy. —Bruno Kreisky

El puerto está cerca, escucho las campanas, toda la gente está exultando. —Walt Whitman

www.elsolucionario.net

Pla n g e ne r a l

24.1 Introducción

24.1 24.2 24.3 24.4 24.5 24.6 24.7 24.8 24.9 24.10 24.11

993

Introducción Manipulación de URLs Cómo leer un archivo en un servidor Web Cómo establecer un servidor simple utilizando sockets de flujo Cómo establecer un cliente simple utilizando sockets de flujo Interacción entre cliente/servidor mediante conexiones de socket de flujo Interacción entre cliente/servidor sin conexión mediante datagramas Juego de Tres en raya (Gato) tipo cliente/servidor, utilizando un servidor con subprocesamiento múltiple La seguridad y la red [Bono Web] Ejemplo práctico: servidor y cliente DeitelMessenger Conclusión

Resumen | Terminología | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

24.1 Introducción Hay mucha emoción en cuanto a Internet y World Wide Web. Internet enlaza la “información de todo el mundo”. World Wide Web facilita el uso de Internet y le proporciona los beneficios de multimedia. Las organizaciones ven a Internet y a la Web como algo crucial para sus estrategias de sistemas de información. Java proporciona una variedad de herramientas de red integradas, las cuales facilitan el desarrollo de aplicaciones basadas en Internet y Web. Java puede permitir a los programas buscar información en todo el mundo y colaborar con programas que se ejecutan en otras computadoras a nivel internacional, nacional o solamente dentro de una organización. Java puede permitir que los applets (subprogramas) y aplicaciones se comuniquen entre sí (lo cual está sujeto a ciertas restricciones de seguridad). Las redes son un tema masivo y complejo. Los estudiantes de ciencias computacionales e ingeniería computacional, por lo general, toman un curso de nivel superior (con duración de un semestre completo) sobre redes de computadoras y continúan estudiando a nivel de graduados. Java se utiliza comúnmente como un vehículo de implementación en cursos de redes computacionales. En este libro le presentamos una porción de los conceptos y herramientas de Java para trabajo en red. En Java, las herramientas de red fundamentales se declaran mediante clases e interfaces del paquete java. net, mediante el cual Java ofrece comunicaciones basadas en flujos que permiten a las aplicaciones ver las redes como flujos de datos. Las clases e interfaces del paquete java.net también ofrecen comunicaciones basadas en paquetes, para transmitir paquetes individuales de información; esto se utiliza comúnmente para transmitir audio y video a través de Internet. En este capítulo mostraremos cómo crear y manipular sockets, y cómo comunicarnos con los paquetes de datos. Nuestra discusión sobre las redes se enfoca en ambos lados de una relación cliente-servidor. El cliente solicita que se realice cierta acción, y el servidor realiza la acción y responde al cliente. Una implementación común del modelo de petición-respuesta se da entre los navegadores y servidores Web. Cuando un usuario selecciona un sitio Web para navegar mediante un navegador (la aplicación cliente), se envía una petición al servidor Web apropiado (la aplicación servidor). Por lo general, el servidor responde al cliente enviando una página Web de HTML apropiada. Presentaremos las comunicaciones basadas en sockets en Java, las cuales permiten a las aplicaciones ver las redes como si fueran E/S de archivo; un programa puede leer de un socket o escribir en un socket de una manera tan simple como leer o escribir en un archivo. El socket es simplemente una construcción de software que representa un extremo de una conexión. Mostraremos cómo crear y manipular sockets de flujo y sockets de datagrama. Con los sockets de flujo, un proceso establece una conexión con otro proceso. Mientras la conexión está en pie, los datos fluyen entre los procesos en flujos continuos. Se dice que los sockets de flujo proporcionan un servicio orientado a la conexión. El protocolo utilizado para la transmisión es el popular TCP (Protocolo de control de transmisión). Con los sockets de datagrama se transmiten paquetes individuales de información. Esto no es adecuado para los programadores cotidianos, ya que el protocolo utilizado (UDP, el Protocolo de datagramas de usuario)

www.elsolucionario.net

994

Capítulo 24 Redes

es un servicio sin conexión y no garantiza que los paquetes lleguen en un orden específico. Con el UDP pueden perderse paquetes, o incluso duplicarse. Se requiere de programación adicional por parte del programador para tratar con estos problemas (si el programador así lo quiere). UDP es más apropiado para aplicaciones de red en las que no se requiere la comprobación de errores y la confiabilidad de TCP. Los sockets de flujo y el protocolo TCP son más convenientes para la gran mayoría de programadores de Java.

Tip de rendimiento 24.1 Los servicios sin conexión generalmente ofrecen un mayor rendimiento, pero son menos confiables que los servicios orientados a las conexiones.

Tip de portabilidad 24.1 TCP, UDP y los protocolos relacionados permiten que una gran variedad de sistemas computacionales heterogéneos (es decir, sistemas computacionales con diferentes procesadores y sistemas operativos) se comuniquen unos con otros.

También presentaremos un ejemplo práctico en el que implementamos una aplicación cliente/servidor para conversar (chat), similar a los populares servicios de mensajería instantánea en Web hoy en día. Este ejemplo práctico se proporciona como bono Web en www.deitel.com/books/jhtp7/. La aplicación incorpora muchas técnicas de red que introduciremos en este capítulo. El programa también introduce el término transmisión múltiple (multicasting), en donde un servidor puede publicar información y los clientes pueden suscribirse a esa información. Cada vez que el servidor publica más información, todos los suscriptores la reciben. A lo largo de los ejemplos de este capítulo veremos que muchos de los detalles del trabajo en red se manejan mediante las APIs de Java.

24.2 Manipulación de URLs Internet ofrece muchos protocolos. El Protocolo de transferencia de hipertexto (HTTP) que forma la base de World Wide Web, utiliza URIs (Identificadores uniformes de recursos) para identificar datos en Internet. Los URIs que especifican las ubicaciones de documentos se conocen como URLs (Localizadores uniformes de recursos). Los URLs comunes hacen referencia a archivos o directorios y pueden hacer referencia a objetos que realizan tareas complejas, como búsquedas en bases de datos y en Internet. Si usted conoce el URL de HTTP de un documento HTML que esté públicamente disponible en Web, puede acceder a esos datos a través de HTTP. Java facilita la manipulación de URLs. Si se utiliza un URL que haga referencia a la ubicación exacta de un recurso (como una página Web) como un argumento para el método showDocument de la interfaz AppletContext, el navegador en el que se ejecuta el applet mostrará ese recurso. El applet de las figuras 24.1 y 24.2 demuestra el uso de las herramientas simples de red. El applet permite al usuario seleccionar una página Web de un objeto JList, y hace que el navegador muestre la página correspondiente. En este ejemplo, el trabajo en red es realizado por el navegador. Este applet aprovecha los parámetros de applet especificados en el documento HTML que invoca al applet. Al navegar por World Wide Web, a menudo se encontrará applets en el dominio público; puede utilizarlos sin costo en sus propias páginas Web (generalmente a cambio de dar créditos al creador del applet). Muchos applets pueden personalizarse mediante los parámetros que se proporcionan desde el archivo HTML que invoca al applet. Por ejemplo, en la figura 24.1 se muestra el HTML que invoca al objeto SelectorSitios en la figura 24.2.

1 2 3 4 5 6 7

Selector de sitios

Figura 24.1 | Documento HTML para cargar el applet SelectorSitios. (Parte 1 de 2).

www.elsolucionario.net

24.2

8 9 10 11 12 13 14 15



Figura 24.1 | Documento HTML para cargar el applet SelectorSitios. (Parte 2 de 2).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46

// Fig. 24.2: SelectorSitios.java // Este programa carga un documento de un URL. import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.ArrayList; import java.awt.BorderLayout; import java.applet.AppletContext; import javax.swing.JApplet; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JScrollPane; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; public class SelectorSitios extends JApplet { private HashMap< Object, URL > sitios; // nombres de sitios y URLs private ArrayList< String > nombresSitios; // nombres de sitios private JList selectorSitios; // lista de sitios a elegir // lee los parámetros de HTML y establece la GUI public void init() { sitios = new HashMap< Object, URL >(); // crea objeto HashMap nombresSitios = new ArrayList< String >(); // crea objeto ArrayList // obtiene los parámetros del documento de HTML obtenerSitiosDeParametrosHTML(); // crea componentes de GUI e interfaz de esquema add( new JLabel( "Seleccione un sitio para navegar" ), BorderLayout.NORTH ); selectorSitios = new JList( nombresSitios.toArray() ); // llena el objeto JList selectorSitios.addListSelectionListener( new ListSelectionListener() // clase interna anónima { // va al sitio seleccionado por el usuario public void valueChanged( ListSelectionEvent evento ) { // obtiene el nombre del sitio seleccionado Object objeto = selectorSitios.getSelectedValue(); // usa el nombre del sitio para localizar el URL correspondiente URL nuevoDocumento = sitios.get( objeto );

Figura 24.2 | Cómo cargar un documento de un URL a un navegador. (Parte 1 de 3).

www.elsolucionario.net

996

47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90

Capítulo 24 Redes

// obtiene el contenedor de applets AppletContext navegador = getAppletContext(); // indica al contenedor de applets que cambie de página navegador.showDocument( nuevoDocumento ); } // fin del método valueChanged } // fin de la clase interna anónima ); // fin de la llamada a addListSelectionListener add( new JScrollPane( selectorSitios ), BorderLayout.CENTER ); } // fin del método init // obtiene los parámetros del documento de HTML private void obtenerSitiosDeParametrosHTML() { String titulo; // titulo del sitio String ubicacion; // ubicacion del sitio URL url; // URL de la ubicación int contador = 0; // cuenta el número de sitios titulo = getParameter( "titulo" + contador ); // obtiene el título del primer sitio // itera hasta que no haya más parámetros en el documento de HTML while ( titulo != null ) { // obtiene la ubicación del sitio ubicacion = getParameter( "ubicacion" + contador ); try // coloca titulo/URL en objeto HashMap y titulo en objeto ArrayList { url = new URL( ubicacion ); // convierte la ubicación en URL sitios.put( titulo, url ); // coloca titulo/URL en objeto HashMap nombresSitios.add( titulo ); // coloca titulo en objeto ArrayList } // fin de try catch ( MalformedURLException excepcionURL ) { excepcionURL.printStackTrace(); } // fin de catch contador++; titulo = getParameter( "titulo" + contador ); // obtiene el título del siguiente sitio } // fin de while } // fin del método obtenerSitiosDeParametrosHTML } // fin de la clase SelectorSitios

Figura 24.2 | Cómo cargar un documento de un URL a un navegador. (Parte 2 de 3).

www.elsolucionario.net

24.2

Manipulación de URLs

997

Figura 24.2 | Cómo cargar un documento de un URL a un navegador. (Parte 3 de 3). El documento HTML contiene ocho parámetros especificados con la marca param; estas líneas deben aparecer entre las marcas applet inicial y final. El applet puede leer estos valores y utilizarlos para personalizarse a sí mismo. Puede aparecer cualquier número de marcas param entre las marcas applet inicial y final. Cada parámetro tiene un nombre y un valor. El método getParameter de Applet obtiene el valor (value) asociado con un nombre de parámetro específico y lo devuelve como una cadena. El argumento que se pasa a getParameter es una cadena que contiene el nombre del parámetro en el elemento param. En este ejemplo, los parámetros representan el título y la ubicación de cada sitio Web que puede seleccionar el usuario. Los parámetros especificados para este applet se nombran com titulo#, en donde el valor de # empieza en 0 y se incrementa en uno para cada nuevo título. Cada título debe tener un parámetro de ubicación correspondiente, de la forma ubicacion#, en donde el valor de # empieza en 0 y se incrementa en uno para cada nueva ubicación. La instrucción String titulo = getParameter( "titulo0" );

obtiene el valor asociado con el parámetro "titulo0" y lo asigna a la referencia titulo. Si no hay una marca param que contenga el parámetro especificado, getParameter devuelve null. El applet (figura 24.2) obtiene del documento HTML (figura 24.1) las opciones a mostrar en el objeto JList del applet. La clase SelectorSitios utiliza un objeto HashMap (paquete java.util) para almacenar los nombres de sitios y sus URLs. En este ejemplo, la clave es la cadena en el objeto JList que representa el nombre del sitio Web, y el valor es un objeto URL que almacena la ubicación del sitio Web que se mostrará en el navegador. La clase SelectorSitios también contiene un objeto ArrayList (paquete java.util) en donde se colocan los nombres de los sitios, de manera que se puedan utilizar para inicializar el objeto JList (una versión del constructor de JList recibe un arreglo de objetos Object, el cual es devuelto por el método toArray de ArrayList. Un objeto ArrayList es un arreglo de referencias que puede cambiar su tamaño en forma dinámica. La clase ArrayList proporciona el método add para agregar un nuevo elemento al final del objeto ArrayList. (En el capítulo 19 hablamos sobre las clases ArrayList y HashMap). En las líneas 25 a 26 del método init del applet (líneas 23 a 57) se crea un objeto HashMap y un objeto ArrayList. En la línea 29 se hace una llamada a nuestro método utilitario obtenerSitiosDeParametrosHTML (declarado en las líneas 60 a 89) para obtener los parámetros de HTML del documento HTML que invocó al applet. En el método obtenerSitiosDeParametrosHTML se utiliza el método getParameter de Applet (línea 67) para obtener el título de un sitio Web. Si el titulo no es null, el ciclo de las líneas 70 a 88 empieza a ejecutarse. En la línea 73 se utiliza el método getParameter de Applet para obtener la ubicación del sitio Web. En la línea 77 se utiliza la ubicacion como el valor de un nuevo objeto URL. El constructor de URL determina

www.elsolucionario.net

998

Capítulo 24 Redes

si su argumento representa a un URL válido. Si no es así, el constructor de URL lanza una excepción MalformedURLException. Observe que el constructor de URL debe ser llamado en un bloque try. Si el constructor de URL genera una excepción MalformedURLException, la llamada a printStackTrace (línea 83) hace que el programa imprima un rastreo de la pila en la consola de Java. En equipos Windows, para ver la consola de Java hay que hacer clic con el botón derecho del ratón en el icono de Java que se encuentra en el área de notificación de la barra de tareas. Después el programa trata de obtener el título del siguiente sitio Web. El programa no agrega el sitio del URL inválido al objeto HashMap, por lo que ese título no se mostrará en el objeto JList. Para un URL correcto, en la línea 78 se colocan el titulo y el URL en el objeto HashMap, y en la línea 79 se agrega el titulo al objeto ArrayList. En la línea 87 se obtiene el siguiente título del documento HTML. Cuando la llamada a getParameter en la línea 87 devuelve null, el ciclo termina. Cuando el método obtenerSitiosDeParametrosHTML regresa a init, en las líneas 32 a 56 se construye la GUI del applet. En la línea 32 se agrega la etiqueta JLabel “Seleccione un sitio para navegar” a la sección NORTH del esquema BorderLayout del objeto JFrame. En la línea 34 se crea un objeto JList llamado selectorSitios para permitir al usuario seleccionar una página Web y verla. En las líneas 35 a 54 se registra un objeto ListSelectionListener para manejar los eventos de selectorSitios. En la línea 56 se agrega selectorSitios a la sección CENTER del esquema BorderLayout del objeto JFrame. Cuando el usuario selecciona uno de los sitios Web listados en selectorSitios, el programa llama al método valueChanged (líneas 39 a 52). En la línea 42 se obtiene del objeto JList el nombre del sitio seleccionado. En la línea 45 se pasa el nombre del sitio seleccionado (la clave) al método get de HashMap, el cual localiza y devuelve una referencia al objeto URL correspondiente (el valor) que se asigna a la referencia nuevoDocumento. En la línea 48 se utiliza el método getAppletContext de Applet para obtener una referencia a un objeto AppletContext que representa el contenedor del applet. En la línea 51 se utiliza la referencia navegador de AppletContext para invocar el método showDocument, el cual recibe un objeto URL como argumento, y lo pasa al objeto AppletContext (es decir, el navegador). El navegador muestra en su ventana actual el recurso Web asociado con ese URL. En este ejemplo, todos los recursos son documentos HTML. Para los programadores familiarizados con los marcos de HTML, hay una segunda versión del método showDocument de AppletContext que permite a un applet especificar lo que se conoce como el marco de destino, en el cual se mostrará el recurso Web. Esta segunda versión recibe dos argumentos: un objeto URL que especifica el recurso a mostrar, y una cadena que representa el marco de destino. Hay algunos marcos de destino especiales que pueden utilizarse como el segundo argumento. El marco de destino _blank ocasiona que se muestre el contenido del URL especificado en una nueva ventana del navegador Web. El marco de destino _self especifica que el contenido del URL especificado debe mostrarse en el mismo marco que el applet (la página HTML del applet se reemplaza en este caso). El marco de destino _top especifica que el navegador debe eliminar los marcos actuales en la ventana del navegador y después mostrar el contenido del URL especificado en la ventana actual. [Nota: si le interesa aprender más acerca de HTML, el CD que se incluye con este libro contiene tres capítulos de nuestro libro Internet and World Wide Web How to Program, Tercera edición, que introducen la versión actual de HTML (conocida como XHTML) y la herramienta para formato de páginas Web conocida como Hojas de estilo en cascada (CSS)].

Tip para prevenir errores 24.1 El applet de la figura 24.2 debe ejecutarse desde un navegador Web como Mozilla o Microsoft Internet Explorer, para que se pueda ver el resultado de mostrar otra página Web. El appletviewer es capaz de ejecutar applets solamente; ignora todas las demás marcas de HTML. Si los sitos Web en el programa incluyeran applets de Java, sólo aparecerían esos applets en el appletviewer cuando el usuario seleccionara un sitio Web. Cada applet se ejecutaría en una ventana separada del appletviewer.

24.3 Cómo leer un archivo en un servidor Web En nuestro siguiente ejemplo, una vez más ocultamos los detalles relacionados con la red. La aplicación de la figura 24.3 utiliza el componente de la GUI de Swing JEditorPane (del paquete javax.swing) para mostrar el contenido de un archivo en un servidor Web. El usuario introduce un URL en el objeto JTextField que se encuentra en la parte superior de la ventana, y la aplicación muestra el documento correspondiente (si es que existe) en el objeto JEditorPane. La clase JEditorPane puede desplegar tanto texto simple como texto con formato HTML (como se muestra en las dos capturas de pantalla de la figura 24.4), por lo que esta aplicación actúa como

www.elsolucionario.net

24.3

Cómo leer un archivo en un servidor Web

999

un navegador Web simple. La aplicación también demuestra cómo procesar eventos HyperlinkEvent cuando el usuario hace clic en un hipervínculo en el documento HTML. Las técnicas que se muestran en este ejemplo también pueden usarse en applets. Sin embargo, a los applets sólo se les permite leer archivos en el servidor del cual se hayan descargado. La clase de aplicación LeerArchivoServidor contiene el objeto JTextField llamado campoIntroducir, en el cual el usuario escribe el URL del archivo a leer, y el objeto JEditorPane llamado areaContenido para mostrar el contenido del archivo. Cuando el usuario oprime la tecla Intro en campoIntroducir, el programa

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

// Fig. 24.3: LeerArchivoServidor.java // Uso de un objeto JEditorPane para mostrar el contenido de un archivo en un servidor Web. import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; import javax.swing.JEditorPane; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; public class LeerArchivoServidor extends JFrame { private JTextField campoIntroducir; // objeto JTextField para escribir el nombre del sitio private JEditorPane areaContenido; // para mostrar un sitio Web // establece la GUI public LeerArchivoServidor() { super( "Navegador Web simple" ); // crea campoIntroducir y registra su componente de escucha campoIntroducir = new JTextField( "Escriba el URL del archivo" ); campoIntroducir.addActionListener( new ActionListener() { // obtiene el documento especificado por el usuario public void actionPerformed( ActionEvent evento ) { obtenerLaPagina( evento.getActionCommand() ); } // fin del método actionPerformed } // fin de la clase interna ); // fin de la llamada a addActionListener add( campoIntroducir, BorderLayout.NORTH ); areaContenido = new JEditorPane(); // crea areaContenido areaContenido.setEditable( false ); areaContenido.addHyperlinkListener( new HyperlinkListener() { // si el usuario hizo clic en un hipervínculo, va a la página especificada public void hyperlinkUpdate( HyperlinkEvent evento ) { if ( evento.getEventType() ==

Figura 24.3 | Cómo leer un archivo, abriendo una conexión a través de un URL. (Parte 1 de 2).

www.elsolucionario.net

1000 Capítulo 24 Redes

49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75

HyperlinkEvent.EventType.ACTIVATED ) obtenerLaPagina( evento.getURL().toString() ); } // fin del método hyperlinkUpdate } // fin de la clase interna anónima ); // fin de la llamada a addHyperlinkListener add( new JScrollPane( areaContenido ), BorderLayout.CENTER ); setSize( 400, 300 ); // establece el tamaño de la ventana setVisible( true ); // muestra la ventana } // fin del constructor de LeerArchivoServidor // carga el documento private void obtenerLaPagina( String ubicacion ) { try // carga el documento y muestra la ubicación { areaContenido.setPage( ubicacion ); // establece la página campoIntroducir.setText( ubicacion ); // establece el texto } // fin de try catch ( IOException excepcionES ) { JOptionPane.showMessageDialog( this, "Error al obtener el URL especificado", "URL incorrecto", JOptionPane.ERROR_MESSAGE ); } // fin de catch } // fin del método obtenerLaPagina } // fin de la clase LeerArchivoServidor

Figura 24.3 | Cómo leer un archivo, abriendo una conexión a través de un URL. (Parte 2 de 2).

1 2 3 4 5 6 7 8 9 10 11 12

// Fig. 24.4: PruebaLeerArchivoServidor.java // Crea e inicia un objeto LeerArchivoServidor. import javax.swing.JFrame; public class PruebaLeerArchivoServidor { public static void main( String args[] ) { LeerArchivoServidor aplicacion = new LeerArchivoServidor(); aplicacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); } // fin de main } // fin de la clase PruebaLeerArchivoServidor

Figura 24.4 | Clase de prueba para LeerArchivoServidor. (Parte 1 de 2).

www.elsolucionario.net

24.4

Cómo establecer un servidor simple utilizando sockets de flujo 1001

Figura 24.4 | Clase de prueba para LeerArchivoServidor. (Parte 2 de 2). llama al método actionPerformed (líneas 31 a 34). En la línea 33 se utiliza el método getActionCommand de ActionEvent para obtener la cadena que introdujo el usuario en el objeto JTextField, y pasa esa cadena al método utilitario obtenerLaPagina (líneas 61 a 74). En la línea 65 se invoca el método setPage de JEditorPane para descargar el documento especificado por ubicacion y mostrarlo en el objeto JEditorPane. Si ocurre un error al descargar el documento, el método setPage lanza una excepción IOException. Además, si se especifica un URL inválido ocurre una excepción MalformedURLException (una subclase de IOException). Si el documento se carga correctamente, en la línea 66 se muestra la ubicación actual en campoIntroducir. Por lo general, un documento HTML contiene hipervínculos (texto, imágenes o componentes de la GUI ) que, cuando se hace clic sobre ellos, proporcionan un acceso rápido a otro documento en Web. Si un objeto JEditorPane contiene un documento HTML y el usuario hace clic en un hipervínculo, el objeto JEditorPane genera un evento HyperlinkEvent (paquete javax.swing.event) y notifica a todos los objetos HyperlinkListener (paquete javax.swing.event) registrados acerca de ese evento. En las líneas 42 a 53 se registra un objeto HyperlinkListener para manejar eventos HyperlinkEvent. Al ocurrir un evento HyperlinkEvent, el programa llama al método hyperlinkUpdate (líneas 46 a 51). En las líneas 48 y 49 se utiliza el método getEventType de HyperlinkEvent para determinar el tipo del evento HyperlinkEvent. La clase HyperlinkEvent contiene una clase anidada public llamada EventType, la cual declara tres objetos EventType estáticos que representan los tipos de eventos de hipervínculos. ACTIVATED indica que el usuario hizo clic en un hipervínculo para cambiar de página Web, ENTERED indica que el usuario movió el ratón sobre un hipervínculo y EXITED indica que el usuario alejó el ratón de un hipervínculo. Si un hipervínculo fue activado (ACTIVATED), en la línea 50 se utiliza el método getURL de HyperlinkEvent para obtener el URL representado por el hipervínculo. El método toString convierte el URL devuelto en una cadena que puede pasarse al método utilitario obtenerLaPagina. Archivo Nuevo Abrir... Cerrar

Observación de apariencia visual 24.1 Un objeto JEditorPane genera eventos HyperlinkEvent solamente si no puede editarse.

24.4 Cómo establecer un servidor simple utilizando sockets de flujo Los dos ejemplos descritos hasta ahora utilizan herramientas de red de alto nivel en Java para la comunicación entre las aplicaciones. En esos ejemplos no es responsabilidad del programador de Java establecer la conexión entre un cliente y un servidor. El primer programa dependió del navegador Web para comunicarse con un servidor Web. El segundo dependió de un objeto JEditorPane para realizar la conexión. En esta sección comenzaremos con nuestra discusión acerca de cómo crear sus propias aplicaciones que puedan comunicarse entre sí.

www.elsolucionario.net

1002 Capítulo 24 Redes

ket.

Para establecer un servidor simple en Java se requieren cinco pasos. El paso 1 es crear un objeto ServerSocUna llamada al constructor de ServerSocket como: ServerSocket servidor = new ServerSocket(

numeroPuerto, longitudCola

);

registra un número de puerto TCP disponible y específica el máximo número de clientes que pueden esperar para conectarse al servidor (es decir, la longitud de la cola). El número de puerto es utilizado por los clientes para localizar la aplicación servidor en el equipo servidor. A menudo, a esto se le conoce como punto de negociación (handshake). Si la cola está llena, el servidor rechaza las conexiones de los clientes. El constructor establece el puerto en donde el servidor espera las conexiones de los clientes; a este proceso se le conoce como enlazar el servidor al puerto. Cada cliente pedirá conectarse con el servidor en este puerto. Sólo una aplicación puede enlazarse a un puerto específico en el servidor, en un momento dado.

Observación de ingeniería de software 24.1 Los números de puerto pueden ser entre 0 y 65,535. Algunos sistemas operativos reservan los números de puertos menores que 1024 para los servicios del sistema (como los servidores de e-mail y World Wide Web). Por lo general, estos puertos no deben especificarse como puertos de conexión en los programas de los usuarios. De hecho, algunos sistemas operativos requieren de privilegios de acceso especiales para enlazarse a los números de puerto menores que 1024.

Los programas administran cada conexión cliente mediante un objeto Socket. En el paso 2, el servidor escucha indefinidamente (o bloquea) para esperar a que un cliente trate de conectarse. Para escuchar una conexión de un cliente, el programa llama al método accept de ServerSocket, como se muestra a continuación: Socket conexion = servidor.accept();

esta instrucción devuelve un objeto Socket cuando se establece la conexión con un cliente. El objeto Socket permite al servidor interactuar con el cliente. Las interacciones con el cliente ocurren realmente en un puerto del servidor distinto al del punto de negociación. De esta forma, el puerto especificado en el paso 1 puede utilizarse nuevamente en un servidor con subprocesamiento múltiple, para aceptar otra conexión cliente. En la sección 24.8 demostraremos este concepto. El paso 3 es obtener los objetos OutputStream e InputStream que permiten al servidor comunicarse con el cliente, enviando y recibiendo bytes. El servidor envía información al cliente mediante un objeto OutputStream y recibe información del cliente mediante un objeto InputStream. El servidor invoca al método getOutputStream en el objeto Socket para obtener una referencia al objeto OutputStream del objeto Socket, e invoca al método getInputStream en el objeto Socket para obtener una referencia al objeto InputStream del objeto Socket. Los objetos flujo pueden utilizarse para enviar o recibir bytes individuales, o secuencias de bytes, mediante el método write de OutputStream y el método read de InputStream, respectivamente. A menudo es útil enviar o recibir valores de tipos primitivos (como int y double) u objetos Serializable (como objetos String u otros tipos serializables), en vez de enviar bytes. En este caso podemos utilizar las técnicas del capítulo 14 para envolver otros tipos de flujos (como ObjectOutputStream y ObjectInputStream) alrededor de los objetos OutputStream e InputStream asociados con el objeto Socket. Por ejemplo, ObjectInputStream entrada = new ObjectInputStream( conexion.getInputStream() ); ObjectOutputStream salida = new ObjectOutputStream( conexion.getOutputStream() );

Lo mejor de establecer estas relaciones es que, cualquier cosa que escriba el servidor en el objeto ObjectOutputStream se enviará mediante el objeto OutputStream y estará disponible en el objeto InputStream del cliente, y cualquier cosa que el cliente escriba en su objeto OutputStream (mediante su correspondiente objeto ObjectOutputStream) estará disponible a través del objeto InputStream del servidor. La transmisión de los datos a través de la red es un proceso transparente, y se maneja completamente mediante Java.

www.elsolucionario.net

24.5

Cómo establecer un cliente simple utilizando sockets de flujo 1003

El paso 4 es la fase de procesamiento, en la cual el servidor y el cliente se comunican a través de los objetos e InputStream. En el paso 5, cuando se completa la transmisión, el servidor cierra la conexión invocando al método close en los flujos y en el objeto Socket.

OutputStream

Observación de ingeniería de software 24.2 Con los sockets, la E/S de red es vista por los programas de Java como algo similar a un archivo de E/S secuencial. Los sockets ocultan al programador gran parte de la complejidad de la programación en red. 18.3

Observación de ingeniería de software 24.3 Mediante el subprocesamiento múltiple en Java, podemos crear servidores que utilicen esta característica y puedan administrar muchas conexiones simultáneas con muchos clientes. Esta arquitectura de servidor con subprocesamiento múltiple es precisamente lo que utilizan los servidores de red populares. 18.4

Observación de ingeniería de software 24.4 Un servidor con subprocesamiento múltiple puede tomar el Socket devuelto por cada llamada al método accept, y puede crear un nuevo subproceso que administre la E/S de red a través de ese objeto Socket. Como alternativa, un servidor con subprocesamiento múltiple puede mantener una reserva de subprocesos (un conjunto de subprocesos ya existentes) listos para administrar la E/S de red a través de los nuevos objetos Socket, a medida que se vayan creando. En el capítulo 23 podrá consultar más información acerca del subprocesamiento múltiple.

Tip de rendimiento 24.2 En los sistemas de alto rendimiento en los que la memoria es abundante, puede implementarse un servidor con subprocesamiento múltiple para crear una reserva de subprocesos que puedan asignarse rápidamente para manejar la E/S de red a través de cada nuevo Socket, a medida que se vayan creando. Por lo tanto, cuando el servidor recibe una conexión, no necesita incurrir en la sobrecarga que se genera debido a la creación de los subprocesos. Cuando se cierra la conexión, el subproceso se devuelve a la reserva para su reutilización.

24.5 Cómo establecer un cliente simple utilizando sockets de flujo Para establecer un cliente simple en Java se requieren cuatro pasos. En el paso 1 creamos un objeto Socket para conectarse al servidor. El constructor de Socket establece la conexión al servidor. Por ejemplo, la instrucción: Socket conexion = new Socket( direccionServidor, puerto ); utiliza el constructor de Socket con dos argumentos: la dirección del servidor (direccionServidor) y el número de puerto. Si el intento de conexión es exitoso, esta instrucción devuelve un objeto Socket. Un intento de conexión fallido lanzará una instancia de una subclase de IOException, por lo que muchos programas simplemente atrapan a IOException. Una excepción UnknownHostException ocurre específicamente cuando el sistema no puede resolver la dirección del servidor especificada en la llamada al constructor de Socket en una dirección IP que corresponda. En el paso 2, el cliente utiliza los métodos getInputStream y getOutputStream de la clase Socket para obtener referencias a los objetos InputStream y OutputStream de Socket. Como dijimos en la sección anterior, podemos utilizar las técnicas del capítulo 14 para envolver otros tipos de flujos alrededor de los objetos InputStream y OutputStream asociados con el objeto Socket. Si el servidor va a enviar información en el formato de los tipos actuales, el cliente debe recibir esa información en el mismo formato. Por lo tanto, si el servidor envía los valores con un objeto ObjectOutputStream, el cliente debe leer esos valores con un objeto ObjectInputStream. El paso 3 es la fase de procesamiento, en la cual el cliente y el servidor se comunican a través de los objetos InputStream y OutputStream. En el paso 4, el cliente cierra la conexión cuando se completa la transmisión, invocando al método close en los flujos y en el objeto Socket. El cliente debe determinar cuándo va a terminar el servidor de enviar información, de manera que pueda llamar al método close para cerrar la conexión del objeto Socket. Por ejemplo, el método read de InputStream devuelve el valor –1 cuando detecta el fin del flujo (lo que se conoce también como EOF: fin del archivo). Si se utiliza un objeto ObjectInputStream para leer información del servidor, se produce una excepción EOFException cuando el cliente trata de leer un valor de un flujo en el que se detectó el fin del flujo.

www.elsolucionario.net

1004 Capítulo 24 Redes

24.6 Interacción entre cliente/servidor mediante conexiones de socket de flujo En las figuras 24.5 y 24.7 utilizamos sockets de flujo para demostrar una aplicación cliente/servidor para conversar simple. El servidor espera un intento de conexión por parte de un cliente. Cuando se conecta un cliente al servidor, la aplicación servidor envía un objeto String al cliente (recuerde que los objetos String son objetos Serializable), indicando que la conexión con el cliente fue exitosa. Después el cliente muestra el mensaje. Ambas aplicaciones cliente y servidor proporcionan campos de texto que permiten al usuario escribir un mensaje y enviarlo de una a otra aplicación. Cuando el cliente o el servidor envían la cadena "TERMINAR", la conexión entre el cliente y el servidor termina. Después el servidor espera a que se conecte otro cliente. La declaración de la clase Servidor aparece en la figura 24.5. La declaración de la clase Cliente aparece en la figura 24.7. Las capturas de pantalla en las que se muestra la ejecución entre el cliente y el servidor aparecen como parte de la figura 24.7.

La clase Servidor El constructor de Servidor (líneas 30 a 55) crea la GUI del servidor, la cual contiene un objeto JTextField y un objeto JTextArea. Servidor muestra su salida en el objeto JTextArea. Cuando se ejecuta el método main (líneas 7 a 12 de la figura 24.6) crea un objeto Servidor, especifica la operación de cierre predeterminada de la ventana y llama al método ejecutarServidor (declarado en las líneas 58 a 87). El método ejecutarServidor configura el servidor para que reciba una conexión y procese una conexión a la vez. En la línea 62 se crea un objeto ServerSocket llamado servidor, para que espere las conexiones. El objeto ServerSocket escucha en el puerto 12345, en espera de que un cliente se conecte. El segundo argumento para el constructor es el número de conexiones que pueden esperar en una cola para conectarse al servidor (100 en este ejemplo). Si la cola está llena cuando un cliente trate de conectarse, el servidor rechazará la conexión.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

// Fig. 24.5: Servidor.java // Establece un servidor que recibe una conexión de un cliente, envía // una cadena al cliente y cierra la conexión. import java.io.EOFException; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.ServerSocket; import java.net.Socket; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JFrame; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.SwingUtilities; public class Servidor extends JFrame { private JTextField campoIntroducir; // recibe como entrada un mensaje del usuario private JTextArea areaPantalla; // muestra información al usuario private ObjectOutputStream salida; // flujo de salida hacia el cliente private ObjectInputStream entrada; // flujo de entrada del cliente private ServerSocket servidor; // socket servidor private Socket conexion; // conexión al cliente private int contador = 1; // contador del número de conexiones

Figura 24.5 | Porción correspondiente al servidor de una conexión cliente/servidor con socket de flujo. (Parte 1 de 4).

www.elsolucionario.net

24.6

29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87

Interacción entre cliente/servidor mediante conexiones de socket de flujo 1005

// establece la GUI public Servidor() { super( "Servidor" ); campoIntroducir = new JTextField(); // crea objeto campoIntroducir campoIntroducir.setEditable( false ); campoIntroducir.addActionListener( new ActionListener() { // envía un mensaje al cliente public void actionPerformed( ActionEvent evento ) { enviarDatos( evento.getActionCommand() ); campoIntroducir.setText( "" ); } // fin del método actionPerformed } // fin de la clase interna anónima ); // fin de la llamada a addActionListener add( campoIntroducir, BorderLayout.NORTH ); areaPantalla = new JTextArea(); // crea objeto areaPantalla add( new JScrollPane( areaPantalla ), BorderLayout.CENTER ); setSize( 300, 150 ); // establece el tamaño de la ventana setVisible( true ); // muestra la ventana } // fin del constructor de Servidor // establece y ejecuta el servidor public void ejecutarServidor() { try // establece el servidor para que reciba conexiones; procesa las conexiones { servidor = new ServerSocket( 12345, 100 ); // crea objeto ServerSocket while ( true ) { try { esperarConexion(); // espera una conexión obtenerFlujos(); // obtiene los flujos de entrada y salida procesarConexion(); // procesa la conexión } // fin de try catch ( EOFException excepcionEOF ) { mostrarMensaje( "\nServidor termino la conexion" ); } // fin de catch finally { cerrarConexion(); // cierra la conexión contador++; } // fin de finally } // fin de while } // fin de try catch ( IOException exepcionES ) { exepcionES.printStackTrace(); } // fin de catch } // fin del método ejecutarServidor

Figura 24.5 | Porción correspondiente al servidor de una conexión cliente/servidor con socket de flujo. (Parte 2 de 4).

www.elsolucionario.net

1006 Capítulo 24 Redes

88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146

// espera a que llegue una conexión, después muestra información sobre ésta private void esperarConexion() throws IOException { mostrarMensaje( "Esperando una conexion\n" ); conexion = servidor.accept(); // permite al servidor aceptar la conexión mostrarMensaje( "Conexion " + contador + " recibida de: " + conexion.getInetAddress().getHostName() ); } // fin del método esperarConexion // obtiene flujos para enviar y recibir datos private void obtenerFlujos() throws IOException { // establece el flujo de salida para los objetos salida = new ObjectOutputStream( conexion.getOutputStream() ); salida.flush(); // vacía el búfer de salida para enviar información del encabezado // establece el flujo de entrada para los objetos entrada = new ObjectInputStream( conexion.getInputStream() ); mostrarMensaje( "\nSe obtuvieron los flujos de E/S\n" ); } // fin del método obtenerFlujos // procesa la conexión con el cliente private void procesarConexion() throws IOException { String mensaje = "Conexion exitosa"; enviarDatos( mensaje ); // envía mensaje de conexión exitosa // habilita campoIntroducir para que el usuario del servidor pueda enviar mensajes setTextFieldEditable( true ); do // procesa los mensajes enviados desde el cliente { try // lee el mensaje y lo muestra en pantalla { mensaje = ( String ) entrada.readObject(); // lee el nuevo mensaje mostrarMensaje( "\n" + mensaje ); // muestra el mensaje } // fin de try catch ( ClassNotFoundException excepcionClaseNoEncontrada ) { mostrarMensaje( "\nSe recibio un tipo de objeto desconocido" ); } // fin de catch } while ( !mensaje.equals( "CLIENTE>>> TERMINAR" ) ); } // fin del método procesarConexion // cierra flujos y socket private void cerrarConexion() { mostrarMensaje( "\nTerminando conexion\n" ); setTextFieldEditable( false ); // deshabilita campoIntroducir try { salida.close(); // cierra flujo de salida entrada.close(); // cierra flujo de entrada conexion.close(); // cierra el socket } // fin de try

Figura 24.5 | Porción correspondiente al servidor de una conexión cliente/servidor con socket de flujo. (Parte 3 de 4).

www.elsolucionario.net

24.6

147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195

Interacción entre cliente/servidor mediante conexiones de socket de flujo 1007

catch ( IOException exepcionES ) { exepcionES.printStackTrace(); } // fin de catch } // fin del método cerrarConexion // envía el mensaje al cliente private void enviarDatos( String mensaje ) { try // envía objeto al cliente { salida.writeObject( "SERVIDOR>>> " + mensaje ); salida.flush(); // envía toda la salida al cliente mostrarMensaje( "\nSERVIDOR>>> " + mensaje ); } // fin de try catch ( IOException exepcionES ) { areaPantalla.append( "\nError al escribir objeto" ); } // fin de catch } // fin del método enviarDatos // manipula areaPantalla en el subproceso despachador de eventos private void mostrarMensaje( final String mensajeAMostrar ) { SwingUtilities.invokeLater( new Runnable() { public void run() // actualiza areaPantalla { areaPantalla.append( mensajeAMostrar ); // adjunta el mensaje } // fin del método run } // fin de la clase interna anónima ); // fin de la llamada a SwingUtilities.invokeLater } // fin del método mostrarMensaje // manipula a campoIntroducir en el subproceso despachador de eventos private void setTextFieldEditable( final boolean editable ) { SwingUtilities.invokeLater( new Runnable() { public void run() // establece la propiedad de edición de campoIntroducir { campoIntroducir.setEditable( editable ); } // fin del método } // fin de la clase interna ); // fin de la llamada a SwingUtilities.invokeLater } // fin del método setTextFieldEditable } // fin de la clase Servidor

Figura 24.5 | Porción correspondiente al servidor de una conexión cliente/servidor con socket de flujo. (Parte 4 de 4).

Error común de programación 24.1 Al especificar un puerto que ya esté en uso, o especificar un número de puerto incorrecto al crear un objeto Serverse produce una excepción BindException. 18.1

Socket,

En la línea 68 se hace una llamada al método esperarConexion (declarado en las líneas 90 a 96) para esperar la conexión de un cliente. Una vez establecida la conexión, en la línea 69 se hace una llamada al método

www.elsolucionario.net

1008 Capítulo 24 Redes

1 2 3 4 5 6 7 8 9 10 11 12 13

// Fig. 24.6: PruebaServidor.java // Prueba la aplicación Servidor. import javax.swing.JFrame; public class PruebaServidor { public static void main( String args[] ) { Servidor aplicacion = new Servidor(); // crea el servidor aplicacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); aplicacion.ejecutarServidor(); // ejecuta la aplicación servidor } // fin de main } // fin de la clase PruebaServidor

Figura 24.6 | Clase de prueba para Servidor. obtenerFlujos (declarado en las líneas 99 a 109) para obtener las referencias a los flujos para la conexión. En la línea 70 se hace una llamada al método procesarConexion (declarado en las líneas 112 a 133) para enviar el mensaje de conexión inicial al cliente, y para procesar todos los mensajes que se reciban del cliente. El bloque finally (líneas 76 a 80) finaliza la conexión del cliente llamando al método cerrarConexion (líneas 136 a 151), incluso aunque haya ocurrido una excepción. El método mostrarMensaje (líneas 169 a 180) es llamado desde estos métodos para utilizar el subproceso despachador de eventos para mostrar los mensajes en el objeto JTextArea de la aplicación. En el método esperarConexion (líneas 90 a 96) se utiliza el método accept de ServerSocket (línea 93) para esperar una conexión de un cliente. Al ocurrir una conexión, el objeto Socket resultante se asigna a conexion. El método accept realiza un bloqueo hasta que se reciba una conexión (es decir, el subproceso en el que se haga la llamada a accept detiene su ejecución hasta que un cliente se conecte). En las líneas 94 y 95 se imprime en pantalla el nombre del equipo host que realizó la conexión. El método getInetAddress de Socket devuelve un objeto InetAddress (paquete java.net), el cual contiene información acerca del equipo cliente. El método getHostName de InetAddress devuelve el nombre de host del equipo cliente. Por ejemplo, hay una dirección IP (127.0.0.1) y nombre de host (localhost) especiales, que son útiles para probar aplicaciones de red en su equipo local [a ésta también se le conoce como dirección de bucle local (loopback)]. Si se hace una llamada a getHostName en un objeto InetAddress que contenga 127.0.0.1, el nombre de host correspondiente que devuelve el método es localhost. El método obtenerFlujos (líneas 99 a 109) obtiene las referencias a los flujos de Socket y los utiliza para inicializar un objeto ObjectOutputStream (línea 102) y un objeto ObjectInputStream (línea 106), respectivamente. Observe la llamada al método flush de ObjectInputStream en la línea 103. Esta instrucción hace que el objeto ObjectOutputStream en el servidor envíe un encabezado de flujo al objeto ObjectInputStream correspondiente del cliente. El encabezado de flujo contiene información como la versión de la serialización de objetos que se va a utilizar para enviar los objetos. Esta información es requerida por el objeto ObjectInputStream, para que pueda prepararse para recibir estos objetos de manera correcta.

Observación de ingeniería de software 24.5 Al utilizar un objeto ObjectOutputStream y un objeto ObjectInputStream para enviar y recibir datos a través de una conexión de red, siempre debe crear primero el objeto ObjectOutputStream y vaciar (mediante el método flush) el flujo, de manera que el objeto ObjectInputStream pueda prepararse para recibir los datos. Esto se requiere sólo en las aplicaciones de red que se comunican utilizando a ObjectOutputStream y ObjectInputStream.

Tip de rendimiento 24.3 Por lo general, los componentes de entrada y salida de una computadora son mucho más lentos que su memoria. Los búferes de salida se utilizan comúnmente para incrementar la eficiencia de una aplicación, al enviar mayores cantidades de datos con menos frecuencia, con lo cual se reduce el número de veces que una aplicación accede a los componentes de entrada y salida de la computadora.

www.elsolucionario.net

24.6

Interacción entre cliente/servidor mediante conexiones de socket de flujo 1009

En la línea 114 del método procesarConexion (líneas 112 a 133) se hace una llamada al método enviarpara enviar la cadena "SERVIDOR>>> Conexión exitosa" al cliente. El ciclo de las líneas 120 a 132 se ejecuta hasta que el servidor recibe el mensaje "CLIENTE>>> TERMINAR." En la línea 124 se utiliza el método readObject de ObjectInputStream para leer un objeto String del cliente. En la línea 125 se invoca el método mostrarMensaje para anexar el mensaje al objeto JTextArea. Cuando termina la transmisión, el método procesarConexion regresa y el programa llama al método cerrarConexion (líneas 136 a 151) para cerrar los flujos asociados con el objeto Socket, y para cerrar también a ese objeto Socket. Después, el servidor espera el siguiente intento de conexión por parte de un cliente, continuando con la línea 68 al principio del ciclo while. Cuando el usuario de la aplicación servidor introduce una cadena en el campo de texto y oprime la tecla Intro, el programa llama al método actionPerformed (líneas 40 a 44), el cual lee la cadena del campo de texto y llama al método utilitario enviarDatos (líneas 154 a 166) para enviar la cadena al cliente. El método enviarDatos escribe el objeto, vacía el búfer de salida y anexa la misma cadena al área de texto en la ventana del servidor. No es necesario invocar a mostrarMensaje para modificar el área de texto aquí, ya que el método enviarDatos es llamado desde un manejador de eventos; por lo tanto, enviarDatos se ejecuta como parte del subproceso despachador de eventos. Observe que el objeto Servidor recibe una conexión, la procesa, cierra la conexión y espera a la siguiente conexión. Un escenario más apropiado sería un objeto Servidor que recibe una conexión, la configura para procesarla como un subproceso de ejecución separado, e inmediatamente se pone en espera de nuevas conexiones. Los subprocesos separados que procesan conexiones existentes pueden seguir ejecutándose, mientras que el Servidor se concentra en nuevas peticiones de conexión. Esto hace al servidor más eficiente, ya que pueden procesarse varias peticiones de clientes en forma concurrente. En la sección 24.8 mostraremos el uso de un servidor con subprocesamiento múltiple. Datos

La clase Cliente Al igual que la clase Servidor, el constructor (líneas 29 a 56) de la clase Cliente (figura 24.7) crea la GUI de la aplicación (un objeto JTextField y un objeto JTextArea). Cliente muestra su salida en el área de texto. Cuando se ejecuta el método main (líneas 7 a 19 de la figura 24.8), se crea una instancia de la clase Cliente, se especifica la operación de cierre predeterminada de la ventana y se hace una llamada al método ejecutarCliente (declarado en las líneas 59 a 79). En este ejemplo, usted puede ejecutar el cliente desde cualquier computadora en Internet, y especificar la dirección IP o nombre de host del equipo servidor como un argumento de línea de comandos para el programa. Por ejemplo, el comando: java Cliente 192.168.1.15

intenta realizar una conexión al objeto Servidor en la computadora que tiene la dirección IP 192.168.1.15. El método ejecutarCliente (líneas 59 a 79) de la clase Cliente establece la conexión con el servidor, procesa los mensajes recibidos del servidor y cierra la conexión cuando termina la comunicación. En la línea 63 se hace una llamada al método conectarAlServidor (declarado en las líneas 82 a 92) para realizar la conexión. Después de realizar la conexión, en la línea 64 se hace una llamada al método obtenerFlujos (declarado en las líneas 95 a 105) para obtener las referencias a los objetos flujo del objeto Socket. Después, en la línea 65 se hace una llamada al método procesarConexion (declarado en las líneas 108 a 126) para recibir y mostrar los mensajes enviados por el servidor. En el bloque finally (líneas 75 a 78) se hace una llamada al método cerrarConexion (líneas 129 a 144) para cerrar los flujos y el objeto Socket, incluso aunque haya ocurrido una excepción. El método mostrarMensaje (líneas 162 a 173) es llamado desde estos métodos para utilizar el subproceso despachador de eventos para mostrar los mensajes en el área de texto de la aplicación.

1 2 3 4 5

// Fig. 24.7: Cliente.java // Cliente que lee y muestra la información que se envía desde un Servidor. import java.io.EOFException; import java.io.IOException; import java.io.ObjectInputStream;

Figura 24.7 | Porción correspondiente al cliente, de una conexión de sockets de flujo entre un cliente y un servidor. (Parte 1 de 5).

www.elsolucionario.net

1010 Capítulo 24 Redes

6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

import import import import import import import import import import import

java.io.ObjectOutputStream; java.net.InetAddress; java.net.Socket; java.awt.BorderLayout; java.awt.event.ActionEvent; java.awt.event.ActionListener; javax.swing.JFrame; javax.swing.JScrollPane; javax.swing.JTextArea; javax.swing.JTextField; javax.swing.SwingUtilities;

public class Cliente extends JFrame { private JTextField campoIntroducir; // introduce la información del usuario private JTextArea areaPantalla; // muestra la información al usuario private ObjectOutputStream salida; // flujo de salida hacia el servidor private ObjectInputStream entrada; // flujo de entrada del servidor private String mensaje = ""; // mensaje del servidor private String servidorChat; // aloja al servidor para esta aplicación private Socket cliente; // socket para comunicarse con el servidor // inicializa el objeto servidorChat y establece la GUI public Cliente( String host ) { super( "Cliente" ); servidorChat = host; // establece el servidor al que se conecta este cliente campoIntroducir = new JTextField(); // crea objeto campoIntroducir campoIntroducir.setEditable( false ); campoIntroducir.addActionListener( new ActionListener() { // envía el mensaje al servidor public void actionPerformed( ActionEvent evento ) { enviarDatos( evento.getActionCommand() ); campoIntroducir.setText( "" ); } // fin del método actionPerformed } // fin de la clase interna anónima ); // fin de la llamada a addActionListener add( campoIntroducir, BorderLayout.NORTH ); areaPantalla = new JTextArea(); // crea objeto areaPantalla add( new JScrollPane( areaPantalla ), BorderLayout.CENTER ); setSize( 300, 150 ); // establece el tamaño de la ventana setVisible( true ); // muestra la ventana } // fin del constructor de Cliente // se conecta al servidor y procesa los mensajes que éste envía public void ejecutarCliente() { try // se conecta al servidor, obtiene flujos, procesa la conexión { conectarAlServidor(); // crea un objeto Socket para hacer la conexión

Figura 24.7 | Porción correspondiente al cliente, de una conexión de sockets de flujo entre un cliente y un servidor. (Parte 2 de 5).

www.elsolucionario.net

24.6

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121

Interacción entre cliente/servidor mediante conexiones de socket de flujo 1011

obtenerFlujos(); // obtiene los flujos de entrada y salida procesarConexion(); // procesa la conexión } // fin de try catch ( EOFException excepcionEOF ) { mostrarMensaje( "\nCliente termino la conexion" ); } // fin de catch catch ( IOException excepcionES ) { excepcionES.printStackTrace(); } // fin de catch finally { cerrarConexion(); // cierra la conexión } // fin de finally } // fin del método ejecutarCliente // se conecta al servidor private void conectarAlServidor() throws IOException { mostrarMensaje( "Intentando realizar conexion\n" ); // crea objeto Socket para hacer conexión con el servidor cliente = new Socket( InetAddress.getByName( servidorChat ), 12345 ); // muestra la información de la conexión mostrarMensaje( "Conectado a: " + cliente.getInetAddress().getHostName() ); } // fin del método conectarAlServidor // obtiene flujos para enviar y recibir datos private void obtenerFlujos() throws IOException { // establece flujo de salida para los objetos salida = new ObjectOutputStream( cliente.getOutputStream() ); salida.flush(); // vacía el búfer de salida para enviar información de encabezado // establece flujo de entrada para los objetos entrada = new ObjectInputStream( cliente.getInputStream() ); mostrarMensaje( "\nSe obtuvieron los flujos de E/S\n" ); } // fin del método obtenerFlujos // procesa la conexión con el servidor private void procesarConexion() throws IOException { // habilita campoIntroducir para que el usuario cliente pueda enviar mensajes establecerCampoEditable( true ); do // procesa los mensajes que se envían desde el servidor { try // lee el mensaje y lo muestra { mensaje = ( String ) entrada.readObject(); // lee nuevo mensaje mostrarMensaje( "\n" + mensaje ); // muestra el mensaje } // fin de try catch ( ClassNotFoundException excepcionClaseNoEncontrada ) {

Figura 24.7 | Porción correspondiente al cliente, de una conexión de sockets de flujo entre un cliente y un servidor. (Parte 3 de 5).

www.elsolucionario.net

1012 Capítulo 24 Redes

122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179

mostrarMensaje( "nSe recibio un tipo de objeto desconocido" ); } // fin de catch } while ( !mensaje.equals( "SERVIDOR>>> TERMINAR" ) ); } // fin del método procesarConexion // cierra flujos y socket private void cerrarConexion() { mostrarMensaje( "\nCerrando conexion" ); establecerCampoEditable( false ); // deshabilita campoIntroducir try { salida.close(); // cierra el flujo de salida entrada.close(); // cierra el flujo de entrada cliente.close(); // cierra el socket } // fin de try catch ( IOException excepcionES ) { excepcionES.printStackTrace(); } // fin de catch } // fin del método cerrarConexion

1

// envía un mensaje al servidor private void enviarDatos( String mensaje ) { try // envía un objeto al servidor { salida.writeObject( "CLIENTE>>> " + mensaje ); salida.flush(); // envía todos los datos a la salida mostrarMensaje( "\nCLIENTE>>> " + mensaje ); } // fin de try catch ( IOException excepcionES ) { areaPantalla.append( "\nError al escribir objeto" ); } // fin de catch } // fin del método enviarDatos // manipula el objeto areaPantalla en el subproceso despachador de eventos private void mostrarMensaje( final String mensajeAMostrar ) { SwingUtilities.invokeLater( new Runnable() { public void run() // actualiza objeto areaPantalla { areaPantalla.append( mensajeAMostrar ); } // fin del método run } // fin de la clase interna anónima ); // fin de la llamada a SwingUtilities.invokeLater } // fin del método mostrarMensaje // manipula a campoIntroducir en el subproceso despachador de eventos private void establecerCampoEditable( final boolean editable ) { SwingUtilities.invokeLater( new Runnable()

Figura 24.7 | Porción correspondiente al cliente, de una conexión de sockets de flujo entre un cliente y un servidor. (Parte 4 de 5).

www.elsolucionario.net

24.6

180 181 182 183 184 185 186 187 188

Interacción entre cliente/servidor mediante conexiones de socket de flujo 1013

{ public void run() // establece la propiedad de edición de campoIntroducir { campoIntroducir.setEditable( editable ); } // fin del método run } // fin de la clase interna anónima ); // fin de la llamada a SwingUtilities.invokeLater } // fin del método establecerCampoEditable } // fin de la clase Cliente

Figura 24.7 | Porción correspondiente al cliente, de una conexión de sockets de flujo entre un cliente y un servidor. (Parte 5 de 5). El método conectarAlServidor (líneas 82 a 92) crea un objeto Socket llamado cliente (línea 87) para establecer una conexión. Este método pasa dos argumentos al constructor de Socket: la dirección IP del equipo servidor y el número de puerto (12345) en donde la aplicación servidor está esperando las conexiones de los clientes. En el primer argumento, el método static getByName de InetAddress devuelve un objeto InetAddress que contiene la dirección IP especificada como argumento en la línea de comandos para la aplicación (o 127.0.0.1 si no se especifican argumentos en la línea de comandos). El método getByName puede recibir una cadena que contiene la dirección IP actual, o el nombre de host del servidor. El primer argumento también podría haberse escrito de otras formas. Para la dirección 127.0.0.1 de localhost, el primer argumento podría especificarse mediante una de las siguientes expresiones: InetAddress.getByName( "localhost" ) InetAddress.getLocalHost()

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

// Fig. 24.8: PruebaCliente.java // Prueba la clase Cliente. import javax.swing.JFrame; public class PruebaCliente { public static void main( String args[] ) { Cliente aplicacion; // declara la aplicación cliente // si no hay argumentos de línea de comandos if ( args.length == 0 ) aplicacion = new Cliente( "127.0.0.1" ); // se conecta a localhost else aplicacion = new Cliente( args[ 0 ] ); // usa args para conectarse aplicacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); aplicacion.ejecutarCliente(); // ejecuta la aplicación cliente } // fin de main } // fin de la clase PruebaCliente

Figura 24.8 | Clase que prueba a Cliente.

www.elsolucionario.net

1014 Capítulo 24 Redes Además, hay versiones del constructor de Socket que reciben una cadena para la dirección IP o nombre de host. El primer argumento podía haberse especificado como "127.0.0.1" o "localhost". Optamos por demostrar la relación cliente/servidor mediante una conexión entre aplicaciones que se ejecutan en la misma computadora (localhost). Por lo general, este primer argumento sería la dirección IP de otra computadora. El objeto InetAddress para otra computadora se puede obtener si especificamos la dirección IP o nombre de host de la computadora como argumento para el método getByName de InetAddress. El segundo argumento del constructor de Socket es el número de puerto del servidor. Este número debe concordar con el número de puerto en el que el servidor esté esperando las conexiones (al cual se le conoce como el punto de negociación, o handshake). Una vez que se realiza la conexión, en las líneas 90 y 91 se muestra un mensaje en el área de texto, indicando el nombre del equipo servidor al cual se conectó el cliente. El objeto Cliente utiliza un objeto ObjectOutputStream para enviar datos al servidor, y un objeto ObjectInputStream para recibir datos del servidor. El método obtenerFlujos (líneas 95 a 105) crea los objetos ObjectOutputStream y ObjectInputStream que utilizan los flujos asociados con el socket del cliente. El método procesarConexion (líneas 108 a 126) contiene un ciclo que se ejecuta hasta que el cliente recibe el mensaje "SERVIDOR>>> TERMINAR". En la línea 117 se lee un objeto String del servidor. En la línea 118 se invoca el método mostrarMensaje para anexar el mensaje al área de texto. Cuando la transmisión termina, el método cerrarConexion (líneas 129 a 144) cierra los flujos y el objeto Socket. Cuando el usuario de la aplicación cliente introduce una cadena en el campo de texto y oprime la tecla Intro, el programa llama al método actionPerformed (líneas 41 a 45) para leer la cadena e invoca al método utilitario enviarDatos (líneas 147 a 159) para enviar la cadena al servidor. El método enviarDatos escribe el objeto, vacía el búfer de salida y anexa la misma cadena al objeto JTextArea en la ventana del cliente. Una vez más, no es necesario invocar al método utilitario mostrarMensaje para modificar el área de texto aquí, ya que el método enviarDatos es llamado desde un manejador de eventos.

24.7 Interacción entre cliente/servidor sin conexión mediante datagramas Hasta este punto hemos hablado sobre la transmisión basada en flujos, orientada a la conexión. Ahora consideremos la transmisión sin conexión mediante datagramas. La transmisión orientada a la conexión es como el sistema telefónico en el que usted marca y recibe una conexión al teléfono de la persona con la que usted desea comunicarse. La conexión se mantiene todo el tiempo que dure su llamada telefónica, incluso aunque usted no esté hablando. La transmisión sin conexión mediante datagramas es un proceso más parecido a la manera en que el correo se transporta mediante el servicio postal. Si un mensaje extenso no cabe en un sobre, usted lo divide en varias piezas separadas que coloca en sobres separados, numerados en forma secuencial. Cada una de las cartas se envía entonces por correo al mismo tiempo. Las cartas podrían llegar en orden, sin orden o tal vez no llegarían (aunque el último caso es raro, suele ocurrir). La persona en el extremo receptor debe reensamblar las piezas del mensaje en orden secuencial, antes de tratar de interpretarlo. Si su mensaje es lo suficientemente pequeño como para caber en un sobre, no tiene que preocuparse por el problema de que el mensaje esté “fuera de secuencia”, pero aún existe la posibilidad de que su mensaje no llegue. Una diferencia entre los datagramas y el correo postal es que pueden llegar duplicados de datagramas al equipo receptor. En las figuras 24.9 a 24.12 utilizamos datagramas para enviar paquetes de información mediante el Protocolo de datagramas de usuario (UDP) entre una aplicación cliente y una aplicación servidor. En la aplicación Cliente (figura 24.11), el usuario escribe un mensaje en un campo de texto y oprime Intro. El programa convierte el mensaje en un arreglo byte y lo coloca en un paquete de datagramas que se envía al servidor. El Servidor (figura 24.9) recibe el paquete y muestra la información que contiene, después lo repite (echo) de vuelta al cliente. Cuando el cliente recibe el paquete, muestra la información que contiene.

La clase Servidor La clase Servidor (figura 24.9) declara dos objetos DatagramPacket que son utilizados por el servidor para enviar y recibir información, y un objeto DatagramSocket que envía y recibe estos paquetes. El constructor de Servidor (líneas 19 a 37) crea la interfaz gráfica de usuario en la que se mostrarán los paquetes de información. A continuación, en la línea 30 se crea el objeto DatagramSocket en un bloque try. En la línea 30 se utiliza el

www.elsolucionario.net

24.7

Interacción entre cliente/servidor sin conexión mediante datagramas 1015

constructor de DatagramSocket que recibe un argumento entero para el número de puerto (5000 en este ejemplo) para enlazar el servidor a un puerto en donde pueda recibir paquetes de los clientes. Los objetos Cliente que envían paquetes a este objeto Servidor especifican el mismo número de puerto en los paquetes que envían. Si el constructor de DatagramSocket no puede enlazar el objeto DatagramSocket al puerto especificado, se lanza una excepción SocketException. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52

// Fig. 24.9: Servidor.java // Servidor que recibe y envía paquetes desde/hacia un cliente. import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; import java.awt.BorderLayout; import javax.swing.JFrame; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.SwingUtilities; public class Servidor extends JFrame { private JTextArea areaPantalla; // muestra los paquetes recibidos private DatagramSocket socket; // socket para conectarse al cliente // establece la GUI y el objeto DatagramSocket public Servidor() { super( "Servidor" ); areaPantalla = new JTextArea(); // crea objeto areaPantalla add( new JScrollPane( areaPantalla ), BorderLayout.CENTER ); setSize( 400, 300 ); // establece el tamaño de la ventana setVisible( true ); // muestra la ventana try // crea objeto DatagramSocket para enviar y recibir paquetes { socket = new DatagramSocket( 5000 ); } // fin de try catch ( SocketException excepcionSocket ) { excepcionSocket.printStackTrace(); System.exit( 1 ); } // fin de catch } // fin del constructor de Servidor // espera a que lleguen los paquetes, muestra los datos y repite el paquete al cliente public void esperarPaquetes() { while ( true ) { try // recibe el paquete, muestra su contenido, devuelve una copia al cliente { byte datos[] = new byte[ 100 ]; // establece un paquete DatagramPacket paqueteRecibir = new DatagramPacket( datos, datos.length ); socket.receive( paqueteRecibir ); // espera a recibir el paquete // muestra la información del paquete recibido

Figura 24.9 | Lado servidor de la computación cliente/servidor sin conexión mediante datagramas. (Parte 1 de 2).

www.elsolucionario.net

1016 Capítulo 24 Redes

53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98

mostrarMensaje( "\nPaquete recibido:" + "\nDe host: " + paqueteRecibir.getAddress() + "\nPuerto host: " + paqueteRecibir.getPort() + "\nLongitud: " + paqueteRecibir.getLength() + "\nContiene:\n\t" + new String( paqueteRecibir.getData(), 0, paqueteRecibir.getLength() ) ); enviarPaqueteAlCliente( paqueteRecibir ); // envía el paquete al cliente } // fin de try catch ( IOException excepcionES ) { mostrarMensaje( excepcionES.toString() + "\n" ); excepcionES.printStackTrace(); } // fin de catch } // fin de while } // fin del método esperarPaquetes // repite el paquete al cliente private void enviarPaqueteAlCliente( DatagramPacket paqueteRecibir ) throws IOException { mostrarMensaje( "\n\nRepitiendo datos al cliente..." ); // crea paquete para enviar DatagramPacket paqueteEnviar = new DatagramPacket( paqueteRecibir.getData(), paqueteRecibir.getLength(), paqueteRecibir.getAddress(), paqueteRecibir.getPort() ); socket.send( paqueteEnviar ); // envía paquete al cliente mostrarMensaje( "Paquete enviado\n" ); } // fin del método enviarPaqueteAlCliente // manipula objeto areaPantalla en el subproceso despachador de eventos private void mostrarMensaje( final String mensajeAMostrar ) { SwingUtilities.invokeLater( new Runnable() { public void run() // actualiza areaPantalla { areaPantalla.append( mensajeAMostrar ); // muestra mensaje } // fin del método run } // fin de la clase interna anónima ); // fin de la llamada a SwingUtilities.invokeLater } // fin del método mostrarMensaje } // fin de la clase Servidor

Figura 24.9 | Lado servidor de la computación cliente/servidor sin conexión mediante datagramas. (Parte 2 de 2).

Error común de programación 24.2 Al especificar un puerto que ya esté en uso, o especificar un número de puerto incorrecto al crear un objeto Datase produce una excepción SocketException.

gramSocket,

El método esperarPaquetes (líneas 40 a 68) de la clase Servidor utiliza un ciclo infinito para esperar a que lleguen los paquetes al Servidor. En las líneas 47 y 48 se crea un objeto DatagramPacket, en el cual puede almacenarse un paquete de información que se haya recibido. El constructor de DatagramPacket para este fin recibe dos argumentos: un arreglo byte en el cual se almacenarán los datos y la longitud del arreglo. En la línea 50 se utiliza el método receive de DatagramSocket para esperar a que llegue un paquete al Servidor. El método

www.elsolucionario.net

24.7

1 2 3 4 5 6 7 8 9 10 11 12 13

Interacción entre cliente/servidor sin conexión mediante datagramas 1017

// Fig. 24.10: PruebaServidor.java // Prueba la clase Servidor. import javax.swing.JFrame; public class PruebaServidor { public static void main( String args[] ) { Servidor aplicacion = new Servidor(); // crea el servidor aplicacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); aplicacion.esperarPaquetes(); // ejecuta la aplicación servidor } // fin de main } // fin de la clase PruebaServidor Ventana del Servidor después de recibir el paquete de datos del Cliente

Figura 24.10 | Clase que prueba el Servidor. receive hace un bloqueo hasta que llega el paquete y después lo almacena en su argumento DatagramPacket. Este método lanza una excepción IOException si ocurre un error al recibir un paquete. Cuando llega un paquete, en las líneas 53 a 58 se hace una llamada al método mostrarMensaje (declarado en las líneas 86 a 97) para anexar el contenido del paquete al área de texto. El método getAddress de DatagramPacket (línea 54) devuelve un objeto InetAddress que contiene el nombre de host del equipo desde el que se envió el paquete. El método getPort (línea 55) devuelve un entero que especifica el número de puerto a través del que el equipo host enviará el paquete. El método getLength (línea 56) devuelve un entero que representa el número de bytes de datos que se enviaron. El método getData (línea 57) devuelve un arreglo byte que contiene los datos. En las líneas 57 y 58 se inicializa un objeto String mediante el uso de un constructor de tres argumentos que recibe un arreglo byte, el desplazamiento y la longitud. Después, este objeto String se anexa al texto que se mostrará en pantalla. Después de mostrar un paquete, en la línea 60 se hace una llamada al método enviarPaqueteAlCliente (declarado en las líneas 71 a 83) para crear un nuevo paquete y enviarlo al cliente. En las líneas 77 a 79 se crea un objeto DatagramPacket y se le pasan cuatro argumentos a su constructor. El primer argumento especifica el arreglo byte a enviar. El segundo especifica el número de bytes a enviar. El tercer argumento especifica la dirección de Internet del equipo cliente a donde se va a enviar el paquete. El cuarto argumento especifica el puerto en el que el cliente espera recibir los paquetes. En la línea 81 se envía el paquete a través de la red. El método send de DatagramSocket lanza una excepción IOException si ocurre un error al enviar un paquete.

La clase Cliente La clase Cliente (figura 24.11) funciona de manera similar a la clase Servidor, excepto que el Cliente envía paquetes sólo cuando el usuario escribe un mensaje en un campo de texto y oprime la tecla Intro. Cuando esto ocurre, el programa llama al método actionPerformed (líneas 32 a 57), el cual convierte la cadena que introdujo el usuario en un arreglo byte (línea 41). En las líneas 44 y 45 se crea un objeto DatagramPacket y se inicializa

www.elsolucionario.net

1018 Capítulo 24 Redes con el arreglo byte, la longitud de la cadena introducida por el usuario, la dirección IP a la que se enviará el paquete (InetAddress.getLocalHost() en este ejemplo) y el número de puerto en el que el Servidor espera los paquetes (5000 en este ejemplo). En la línea 47 se envía el paquete. Observe que el cliente en este ejemplo debe saber que el servidor está recibiendo paquetes en el puerto 5000; de no ser así, el servidor no los recibirá.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52

// Fig. 24.11: Cliente.java // Cliente que envía y recibe paquetes desde/hacia un servidor. import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JFrame; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.SwingUtilities; public class Cliente extends JFrame { private JTextField campoIntroducir; // para introducir mensajes private JTextArea areaPantalla; // para mostrar mensajes private DatagramSocket socket; // socket para conectarse al servidor // establece la GUI y el objeto DatagramSocket public Cliente() { super( "Cliente" ); campoIntroducir = new JTextField( "Escriba aqui el mensaje" ); campoIntroducir.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent evento ) { try // crea y envía un paquete { // obtiene mensaje del campo de texto String mensaje = evento.getActionCommand(); areaPantalla.append( "\nEnviando paquete que contiene: " + mensaje + "\n" ); byte datos[] = mensaje.getBytes(); // convierte en bytes // crea objeto sendPacket DatagramPacket paqueteEnviar = new DatagramPacket( datos, datos.length, InetAddress.getLocalHost(), 5000 ); socket.send( paqueteEnviar ); // envía el paquete areaPantalla.append( "Paquete enviado\n" ); areaPantalla.setCaretPosition( areaPantalla.getText().length() ); } // fin de try catch ( IOException excepcionES )

Figura 24.11 | Lado cliente de la computación cliente/servidor sin conexión mediante datagramas. (Parte 1 de 3).

www.elsolucionario.net

24.7

53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111

Interacción entre cliente/servidor sin conexión mediante datagramas 1019

{ mostrarMensaje( excepcionES.toString() + "\n" ); excepcionES.printStackTrace(); } // fin de catch } // fin de actionPerformed } // fin de la clase interna ); // fin de la llamada a addActionListener add( campoIntroducir, BorderLayout.NORTH ); areaPantalla = new JTextArea(); add( new JScrollPane( areaPantalla ), BorderLayout.CENTER ); setSize( 400, 300 ); // establece el tamaño de la ventana setVisible( true ); // muestra la ventana try // crea objeto DatagramSocket para enviar y recibir paquetes { socket = new DatagramSocket(); } // fin de try catch ( SocketException excepcionSocket ) { excepcionSocket.printStackTrace(); System.exit( 1 ); } // fin de catch } // fin del constructor de Cliente // espera a que lleguen los paquetes del public void esperarPaquetes() { while ( true ) { try // recibe paquete y muestra su { byte datos[] = new byte[ 100 ]; DatagramPacket paqueteRecibir = datos, datos.length );

servidor, muestra el contenido de éstos

contenido // establece el paquete new DatagramPacket(

socket.receive( paqueteRecibir ); // espera el paquete // muestra el contenido del paquete mostrarMensaje( "\nPaquete recibido:" + "\nDe host: " + paqueteRecibir.getAddress() + "\nPuerto host: " + paqueteRecibir.getPort() + "\nLongitud: " + paqueteRecibir.getLength() + "\nContiene:\n\t" + new String( paqueteRecibir.getData(), 0, paqueteRecibir.getLength() ) ); } // fin de try catch ( IOException excepcion ) { mostrarMensaje( excepcion.toString() + "\n" ); excepcion.printStackTrace(); } // fin de catch } // fin de while } // fin del método esperarPaquetes // manipula objeto areaPantalla en el subproceso despachador de eventos private void mostrarMensaje( final String mensajeAMostrar ) {

Figura 24.11 | Lado cliente de la computación cliente/servidor sin conexión mediante datagramas. (Parte 2 de 3).

www.elsolucionario.net

1020 Capítulo 24 Redes

112 113 114 115 116 117 118 119 120 121 122

}

SwingUtilities.invokeLater( new Runnable() { public void run() // actualiza objeto areaPantalla { areaPantalla.append( mensajeAMostrar ); } // fin del método run } // fin de la clase interna ); // fin de la llamada a SwingUtilities.invokeLater } // fin del método mostrarMensaje // fin de la clase Cliente

Figura 24.11 | Lado cliente de la computación cliente/servidor sin conexión mediante datagramas. (Parte 3 de 3). Observe que la llamada al constructor de DatagramSocket (línea 71) en esta aplicación no especifica ningún argumento. Este constructor sin argumentos permite a la computadora seleccionar el siguiente número de puerto disponible para el objeto DatagramSocket. El cliente no necesita un número de puerto específico, ya que el servidor recibe el número de puerto del cliente como parte de cada objeto DatagramPacket enviado por el cliente. Por lo tanto, el servidor puede enviar paquetes de vuelta al mismo equipo y número de puerto desde el cual haya recibido un paquete de información. El método esperarPaquetes (líneas 81 a 107) de la clase Cliente utiliza un ciclo infinito para esperar a que lleguen los paquetes del servidor. En la línea 91 se hace un bloqueo hasta que llegue un paquete. Esto no impide al usuario enviar un paquete, ya que los eventos de la GUI se manejan en el subproceso despachador de eventos. Sólo impide que el ciclo while continúe ejecutándose, hasta que llegue un paquete al Cliente. Cuando llega un paquete, en la línea 91 se almacena este paquete en paqueteRecibir, y en las líneas 94 a 99 se hace una llamada al método mostrarMensaje (declarado en las líneas 110 a 121) para mostrar el contenido del paquete en el área de texto. 1 2 3 4 5 6 7 8 9 10 11 12 13

// Fig. 24.12: PruebaCliente.java // Prueba la clase Cliente. import javax.swing.JFrame; public class PruebaCliente { public static void main( String args[] ) { Cliente aplicacion = new Cliente(); // crea el cliente aplicacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); aplicacion.esperarPaquetes(); // ejecuta la aplicación cliente } // fin de main } // fin de la clase PruebaCliente Ventana del lado del Cliente después de enviar el paquete del Servidor y recibirlo de regreso

Figura 24.12 | Clase que prueba el Cliente.

www.elsolucionario.net

24.8

Juego de Tres en raya (Gato) tipo cliente/servidor, utilizando un servidor con subprocesamiento... 1021

24.8 Juego de Tres en raya (Gato) tipo cliente/servidor, utilizando un servidor con subprocesamiento múltiple En esta sección presentaremos el popular juego de Tres en raya (Gato o Tic-Tac-Toe), que implementaremos mediante el uso de las técnicas cliente/servidor con sockets de flujo. El programa consiste en una aplicación TresEnRaya (figuras 24.13 y 24.14) que permite a dos aplicaciones ClienteTresEnRaya (figuras 24.15 y 24.16) conectarse al servidor y jugar Tres en raya. En la figura 24.17 se muestran pantallas de ejemplo de la salida de este programa.

La clase ServidorTresEnRaya A medida que el ServidorTresEnRaya recibe la conexión de cada cliente, crea una instancia de la clase interna Jugador (líneas 182 a 304 de la figura 24.13) para procesar al cliente en un subproceso separado. Estos subprocesos permiten a los clientes jugar en forma independiente. El primer cliente que se conecta al servidor es el jugador X y el segundo es el jugador O. El jugador X realiza el primer movimiento. El servidor mantiene la información acerca del tablero, para poder determinar si el movimiento de un jugador es válido o inválido.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

// Fig. 24.13: ServidorTresEnRaya.java // Esta clase mantiene un juego de Tres en raya para dos clientes. import java.awt.BorderLayout; import java.net.ServerSocket; import java.net.Socket; import java.io.IOException; import java.util.Formatter; import java.util.Scanner; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.Condition; import javax.swing.JFrame; import javax.swing.JTextArea; import javax.swing.SwingUtilities; public class ServidorTresEnRaya extends JFrame { private String[] tablero = new String[ 9 ]; // tablero de tres en raya private JTextArea areaSalida; // para imprimir los movimientos en pantalla private Jugador[] jugadores; // arreglo de objetos Jugador private ServerSocket servidor; // socket servidor para conectarse con los clientes private int jugadorActual; // lleva la cuenta del jugador que sigue en turno private final static int JUGADOR_X = 0; // constante para el primer jugador private final static int JUGADOR_O = 1; // constante para el segundo jugador private final static String[] MARCAS = { "X", "O" }; // arreglo de marcas private ExecutorService ejecutarJuego; // ejecuta a los jugadores private Lock bloqueoJuego; // para bloquear el juego y estar sincronizado private Condition otroJugadorConectado; // para esperar al otro jugador private Condition turnoOtroJugador; // para esperar el turno del otro jugador // establece servidor de tres en raya y GUI para mostrar mensajes en pantalla public ServidorTresEnRaya() { super( "Servidor de Tres en raya" ); // establece el título de la ventana // crea objeto ExecutorService con un subproceso para cada jugador ejecutarJuego = Executors.newFixedThreadPool( 2 ); bloqueoJuego = new ReentrantLock(); // crea bloqueo para el juego

Figura 24.13 | Lado servidor del programa Tres en raya cliente/servidor. (Parte 1 de 6).

www.elsolucionario.net

1022 Capítulo 24 Redes

41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99

// variable de condición para los dos jugadores conectados otroJugadorConectado = bloqueoJuego.newCondition(); // variable de condición para el turno del otro jugador turnoOtroJugador = bloqueoJuego.newCondition(); for ( int i = 0; i < 9; i++ ) tablero[ i ] = new String( "" ); // crea tablero de tres en raya jugadores = new Jugador[ 2 ]; // crea arreglo de jugadores jugadorActual = JUGADOR_X; // establece el primer jugador como el jugador actual try { servidor = new ServerSocket( 12345, 2 ); // establece objeto ServerSocket } // fin de try catch ( IOException excepcionES ) { excepcionES.printStackTrace(); System.exit( 1 ); } // fin de catch areaSalida = new JTextArea(); // crea objeto JTextArea para mostrar la salida add( areaSalida, BorderLayout.CENTER ); areaSalida.setText( "Servidor esperando conexiones\n" ); setSize( 300, 300 ); // establece el tamaño de la ventana setVisible( true ); // muestra la ventana } // fin del constructor de ServidorTresEnRaya // espera dos conexiones para poder jugar public void execute() { // espera a que se conecte cada cliente for ( int i = 0; i < jugadores.length; i++ ) { try // espera la conexión, crea el objeto Jugador, inicia objeto Runnable { jugadores[ i ] = new Jugador( servidor.accept(), i ); ejecutarJuego.execute( jugadores[ i ] ); // ejecuta el objeto Runnable jugador } // fin de try catch ( IOException excepcionES ) { excepcionES.printStackTrace(); System.exit( 1 ); } // fin de catch } // fin de for bloqueoJuego.lock(); // bloquea el juego para avisar al subproceso del jugador X try { jugadores[ JUGADOR_X ].establecerSuspendido( false ); // continúa el jugador X otroJugadorConectado.signal(); // despierta el subproceso del jugador X } // fin de try finally { bloqueoJuego.unlock(); // desbloquea el juego después de avisar al jugador X } // fin de finally

Figura 24.13 | Lado servidor del programa Tres en raya cliente/servidor. (Parte 2 de 6).

www.elsolucionario.net

24.8

100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155

Juego de Tres en raya (Gato) tipo cliente/servidor, utilizando un servidor con subprocesamiento... 1023

} // fin del método execute // muestra un mensaje en objeto areaSalida private void mostrarMensaje( final String mensajeAMostrar ) { // muestra un mensaje del subproceso de ejecución despachador de eventos SwingUtilities.invokeLater( new Runnable() { public void run() // actualiza el objeto areaSalida { areaSalida.append( mensajeAMostrar ); // agrega el mensaje } // fin del método run } // fin de la clase interna ); // fin de la llamada a SwingUtilities.invokeLater } // fin del método mostrarMensaje // determina si el movimiento es válido public boolean validarYMover( int ubicacion, int jugador ) { // mientras no sea el jugador actual, debe esperar su turno while ( jugador != jugadorActual ) { bloqueoJuego.lock(); // bloquea el juego para esperar a que el otro jugador haga su movmiento try { turnoOtroJugador.await(); // espera el turno de jugador } // fin de try catch ( InterruptedException excepcion ) { excepcion.printStackTrace(); } // fin de catch finally { bloqueoJuego.unlock(); // desbloquea el juego después de esperar } // fin de finally } // fin de while // si la ubicación no está ocupada, realiza el movimiento if ( !estaOcupada( ubicacion ) ) { tablero[ ubicacion ] = MARCAS[ jugadorActual ]; // establece el movimiento en el tablero jugadorActual = ( jugadorActual + 1 ) % 2; // cambia el jugador // deja que el nuevo jugador sepa que se realizó un movimiento jugadores[ jugadorActual ].otroJugadorMovio( ubicacion ); bloqueoJuego.lock(); // bloquea el juego para indicar al otro jugador que realice su movimiento try { turnoOtroJugador.signal(); // indica al otro jugador que debe continuar } // fin de try finally {

Figura 24.13 | Lado servidor del programa Tres en raya cliente/servidor. (Parte 3 de 6).

www.elsolucionario.net

1024 Capítulo 24 Redes

156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214

bloqueoJuego.unlock(); // desbloquea el juego despues de avisar } // fin de finally return true; // notifica al jugador que el movimiento fue válido } // fin de if else // el movimiento no fue válido return false; // notifica al jugador que el movimiento fue inválido } // fin del método validarYMover // determina si la ubicación está ocupada public boolean estaOcupada( int ubicacion ) { if ( tablero[ ubicacion ].equals( MARCAS[ JUGADOR_X ] ) || tablero [ ubicacion ].equals( MARCAS[ JUGADOR_O ] ) ) return true; // la ubicación está ocupada else return false; // la ubicación no está ocupada } // fin del método estaOcupada // coloca código en este método para determinar si terminó el juego public boolean seTerminoJuego() { return false; // esto se deja como ejercicio } // fin del método seTerminoJuego // la clase interna privada Jugador maneja a cada Jugador como objeto Runnable private class Jugador implements Runnable { private Socket conexion; // conexión con el cliente private Scanner entrada; // entrada del cliente private Formatter salida; // salida al cliente private int numeroJugador; // rastrea cuál jugador es el actual private String marca; // marca para este jugador private boolean suspendido = true; // indica si el subproceso está suspendido // establece el subproceso Jugador public Jugador( Socket socket, int numero ) { numeroJugador = numero; // almacena el número de este jugador marca = MARCAS[ numeroJugador ]; // especifica la marca del jugador conexion = socket; // almacena socket para el cliente try // obtiene los flujos del objeto Socket { entrada = new Scanner( conexion.getInputStream() ); salida = new Formatter( conexion.getOutputStream() ); } // fin de try catch ( IOException excepcionES ) { excepcionES.printStackTrace(); System.exit( 1 ); } // fin de catch } // fin del constructor de Jugador // envía mensaje que indica que el otro jugador hizo un movimiento public void otroJugadorMovio( int ubicacion ) { salida.format( "El oponente realizo movimiento\n" ); salida.format( "%d\n", ubicacion ); // envía la ubicación del movimiento

Figura 24.13 | Lado servidor del programa Tres en raya cliente/servidor. (Parte 4 de 6).

www.elsolucionario.net

24.8

215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272

Juego de Tres en raya (Gato) tipo cliente/servidor, utilizando un servidor con subprocesamiento... 1025

salida.flush(); // vacía la salida } // fin del método otroJugadorMovio // controla la ejecución del subproceso public void run() { // envía al cliente su marca (X o O), procesa los mensajes del cliente try { mostrarMensaje( "Jugador " + marca + " conectado\n" ); salida.format( "%s\n", marca ); // envía la marca del jugador salida.flush(); // vacía la salida // si es el jugador X, espera a que llegue el otro jugador if ( numeroJugador == JUGADOR_X ) { salida.format( "%s\n%s", "Jugador X conectado", "Esperando al otro jugador\n" ); salida.flush(); // vacía la salida bloqueoJuego.lock(); // bloquea el juego para esperar al segundo jugador try { while( suspendido ) { otroJugadorConectado.await(); // espera al jugador O } // fin de while } // fin de try catch ( InterruptedException excepcion ) { excepcion.printStackTrace(); } // fin de catch finally { bloqueoJuego.unlock(); // desbloquea el juego después del segundo jugador } // fin de finally // envía un mensaje que indica que el otro jugador se conectó salida.format( "El otro jugador se conecto. Ahora es su turno.\n" ); salida.flush(); // vacía la salida } // fin de if else { salida.format( "El jugador O se conecto, por favor espere\n" ); salida.flush(); // vacía la salida } // fin de else // mientras el juego no termine while ( !seTerminoJuego() ) { int ubicacion = 0; // inicializa la ubicación del movimiento if ( entrada.hasNext() ) ubicacion = entrada.nextInt(); // obtiene la ubicación del movimiento // comprueba si el movimiento es válido if ( validarYMover( ubicacion, numeroJugador ) )

Figura 24.13 | Lado servidor del programa Tres en raya cliente/servidor. (Parte 5 de 6).

www.elsolucionario.net

1026 Capítulo 24 Redes

273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305

{ mostrarMensaje( "\nubicacion: " + ubicacion ); salida.format( "Movimiento valido.\n" ); // notifica al cliente salida.flush(); // vacía la salida } // fin de if else // el movimiento fue inválido { salida.format( "Movimiento invalido, intente de nuevo\n" ); salida.flush(); // vacía la salida } // fin de else } // fin de while } // fin de try finally { try { conexion.close(); // cierra la conexión con el cliente } // fin de try catch ( IOException excepcionES ) { excepcionES.printStackTrace(); System.exit( 1 ); } // fin de catch } // fin de finally } // fin del método run // establece si se suspende el subproceso o no public void establecerSuspendido( boolean estado ) { suspendido = estado; // establece el valor de suspendido } // fin del método establecerSuspendido } // fin de la clase Jugador } // fin de la clase ServidorTresEnRaya

Figura 24.13 | Lado servidor del programa Tres en raya cliente/servidor. (Parte 6 de 6). Vamos a empezar con una discusión sobre el lado servidor del juego de Tres en raya. Al ejecutarse la aplicación ServidorTresEnRaya, el método main (líneas 7 a 12 de la figura 24.14) crea un objeto ServidorTresEnRaya llamado aplicacion. El constructor (líneas 34 a 69 de la figura 24.13) trata de establecer un objeto ServerSocket. Si tiene éxito, el programa muestra la ventana del servidor y después main invoca al método execute (líneas 72 a 100) de ServidorTresEnRaya. El método execute itera dos veces, haciendo un bloqueo en la línea 79 cada vez que espera la conexión de un cliente. Cuando un cliente se conecta, en la línea 79 se crea un nuevo objeto Jugador para administrar la conexión como un subproceso separado, y en la línea 80 se ejecuta el objeto Jugador en la reserva de subprocesos ejecutarJuego. Cuando el ServidorTresEnRaya crea a un Jugador, el constructor de Jugador (líneas 192 a 208) recibe el objeto Socket que representa la conexión con el cliente y obtiene los flujos de entrada y salida asociados. En la línea 201 se crea un objeto Formatter (vea el capítulo 28) y se envuelve alrededor del flujo de salida del socket. El método run de Jugador (líneas 219 a 297) controla la información que se envía al cliente y la información que se recibe del cliente. Primero, pasa al cliente el carácter que va a colocar en el tablero cuando se haga un movimiento (línea 225). En la línea 226 se hace una llamada al método f lush de Formatter para forzar esta salida al cliente. En la línea 241 se suspende el subproceso del jugador X cuando empieza a ejecutarse, ya que el jugador X podrá realizar movimientos sólo hasta después de que el jugador O se conecte. Una vez que se conecta el jugador O se puede empezar a jugar, y el método run empieza a ejecutar su estructura while (líneas 264 a 283). En cada iteración de este ciclo se lee un entero (línea 269) que representa la ubicación en donde el cliente desea colocar una marca, y en la línea 272 se invoca al método validarYMover (declarado en las líneas 118 a 163) de ServidorTresEnRaya para comprobar el movimiento. Si el movimiento es válido, en la línea 275 se envía un mensaje al cliente, indicando que el movimiento fue válido. En caso contrario,

www.elsolucionario.net

24.8

Juego de Tres en raya (Gato) tipo cliente/servidor, utilizando un servidor con subprocesamiento... 1027

en la línea 280 se envía un mensaje al cliente indicando que el movimiento fue inválido. El programa mantiene las ubicaciones en el tablero como números del 0 al 8 (del 0 al 2 para la primera fila, del 3 al 5 para la segunda y del 6 al 8 para la tercera).

1 2 3 4 5 6 7 8 9 10 11 12 13

// Fig. 24.14: PruebaServidorTresEnRaya.java // Prueba el ServidorTresEnRaya. import javax.swing.JFrame; public class PruebaServidorTresEnRaya { public static void main( String args[] ) { ServidorTresEnRaya aplicacion = new ServidorTresEnRaya(); aplicacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); aplicacion.execute(); } // fin de main } // fin de la clase PruebaServidorTresEnRaya

Figura 24.14 | Clase que prueba el servidor de Tres en raya. El método validarYMover (líneas 118 a 163 en la clase ServidorTresEnRaya) sólo permite que se mueva un jugador en un momento dado, con lo cual se evita que ambos jugadores modifiquen la información de estado del juego al mismo tiempo. Si el Jugador que trata de validar un movimiento no es el jugador actual (es decir, el que puede hacer un movimiento), ese Jugador se coloca en estado de espera hasta que sea su turno para realizar un movimiento. Si la ubicación para el movimiento que se está validando ya está ocupada en el tablero, validarYMover devuelve false. En caso contrario, el servidor coloca una marca para el jugador en su representación local del tablero (línea 142), notifica al otro objeto Jugador (línea 146) que se ha realizado un movimiento (para que se pueda enviar un mensaje al cliente), invoca al método signal (línea 152) para que el Jugador en espera (si hay uno) pueda validar un movimiento y devuelve true (línea 159) para indicar que el movimiento es válido.

La clase ClienteTresEnRaya Cada aplicación ClienteTresEnRaya (figura 24.15) mantiene su propia versión de GUI del tablero de Tres en raya en el que se muestra el estado del juego. Los clientes pueden colocar una marca solamente en un cuadro vacío en el tablero. La clase interna Cuadro (líneas 205 a 262 de la figura 24.15) implementa a cada uno de los nueve cuadros en el tablero. Cuando un objeto ClienteTresEnRaya comienza a ejecutarse, crea un objeto JTextArea en el cual se muestran los mensajes del servidor, junto con una representación del tablero en la que se muestran nueve objetos Cuadro. El método iniciarCliente (líneas 80 a 100) abre una conexión con el servidor y obtiene los flujos de entrada y salida asociados del objeto Socket. En las líneas 85 y 86 se realiza una conexión con el servidor. La clase ClienteTresEnRaya implementa a la interfaz Runnable, por lo que un subproceso separado

www.elsolucionario.net

1028 Capítulo 24 Redes puede leer los mensajes del servidor. Este método permite al usuario interactuar con el tablero (en el subproceso despachador de eventos) mientras espera mensajes del servidor. Después de establecer la conexión con el servidor, en la línea 99 se ejecuta el cliente con el objeto ExecutorService llamado trabajador. El método run (líneas 103 a 126) controla el subproceso de ejecución separado. Primero, el método lee el carácter de la marca (X u O) del servidor (línea 105), después itera continuamente (líneas 121 a 125) y lee los mensajes del servidor (línea 124). Cada mensaje se pasa al método procesarMensaje (líneas 129 a 156) para su procesamiento.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50

// Fig. 24.15: ClienteTresEnRaya.java // Cliente que permite a un usuario jugar Tres en raya con otro a través de una red. import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Graphics; import java.awt.GridLayout; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.net.Socket; import java.net.InetAddress; import java.io.IOException; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.SwingUtilities; import java.util.Formatter; import java.util.Scanner; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; public class ClienteTresEnRaya extends JFrame implements Runnable { private JTextField campoId; // campo de texto para mostrar la marca del jugador private JTextArea areaPantalla; // objeto JTextArea para mostrar la salida private JPanel panelTablero; // panel para el tablero de tres en raya private JPanel panel2; // panel que contiene el tablero private Cuadro tablero[][]; // tablero de tres en raya private Cuadro cuadroActual; // el cuadro actual private Socket conexion; // conexión con el servidor private Scanner entrada; // entrada del servidor private Formatter salida; // salida al servidor private String hostTresEnRaya; // nombre de host para el servidor private String miMarca; // la marca de este cliente private boolean miTurno; // determina de qué cliente es el turno private final String MARCA_X = "X"; // marca para el primer cliente private final String MARCA_O = "O"; // marca para el segundo cliente // establece la interfaz de usuario y el tablero public ClienteTresEnRaya( String host ) { hostTresEnRaya = host; // establece el nombre del servidor areaPantalla = new JTextArea( 4, 30 ); // establece objeto JTextArea areaPantalla.setEditable( false ); add( new JScrollPane( areaPantalla ), BorderLayout.SOUTH ); panelTablero = new JPanel(); // establece panel para los cuadros en el tablero panelTablero.setLayout( new GridLayout( 3, 3, 0, 0 ) );

Figura 24.15 | Lado cliente del programa Tres en raya cliente/servidor. (Parte 1 de 5).

www.elsolucionario.net

24.8

51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108

Juego de Tres en raya (Gato) tipo cliente/servidor, utilizando un servidor con subprocesamiento... 1029

tablero = new Cuadro[ 3 ][ 3 ]; // crea el tablero // itera a través de las filas en el tablero for ( int fila = 0; fila < tablero.length; fila++ ) { // itera a través de las columnas en el tablero for ( int columna = 0; columna < tablero[ fila ].length; columna++ ) { // crea un cuadro tablero[ fila ][ columna ] = new Cuadro( ' ', fila * 3 + columna ); panelTablero.add( tablero[ fila ][ columna ] ); // agrega el cuadro } // fin de for interior } // fin de for exterior campoId = new JTextField(); // establece campo de texto campoId.setEditable( false ); add( campoId, BorderLayout.NORTH ); panel2 = new JPanel(); // establece el panel que contiene a panelTablero panel2.add( panelTablero, BorderLayout.CENTER ); // agrega el panel del tablero add( panel2, BorderLayout.CENTER ); // agrega el panel contenedor setSize( 325, 225 ); // establece el tamaño de la ventana setVisible( true ); // muestra la ventana iniciarCliente(); } // fin del constructor de ClienteTresEnRaya // inicia el subproceso cliente public void iniciarCliente() { try // se conecta al servidor, obtiene los flujos e inicia subproceso de salida { // realiza conexión con el servidor conexion = new Socket( InetAddress.getByName( hostTresEnRaya ), 12345 ); // obtiene flujos para entrada y salida entrada = new Scanner( conexion.getInputStream() ); salida = new Formatter( conexion.getOutputStream() ); } // fin de try catch ( IOException excepcionES ) { excepcionES.printStackTrace(); } // fin de catch // crea e inicia subproceso trabajador para este cliente ExecutorService trabajador = Executors.newFixedThreadPool( 1 ); trabajador.execute( this ); // ejecuta el cliente } // fin del método iniciarCliente // subproceso de control que permite la actualización continua de areaPantalla public void run() { miMarca = entrada.nextLine(); // obtiene la marca del jugador (X o O) SwingUtilities.invokeLater( new Runnable()

Figura 24.15 | Lado cliente del programa Tres en raya cliente/servidor. (Parte 2 de 5).

www.elsolucionario.net

1030 Capítulo 24 Redes

109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167

{ public void run() { // muestra la marca del jugador campoId.setText( "Usted es el jugador \"" + miMarca + "\"" ); } // fin del método run } // fin de la clase interna anónima ); // fin de la llamada a SwingUtilities.invokeLater miTurno = ( miMarca.equals( MARCA_X ) ); // determina si es turno del cliente // recibe los mensajes que se envían al cliente y los imprime en pantalla while ( true ) { if ( entrada.hasNextLine() ) procesarMensaje( entrada.nextLine() ); } // fin de while } // fin del método run // procesa los mensajes recibidos por el cliente private void procesarMensaje( String mensaje ) { // ocurrió un movimiento válido if ( mensaje.equals( "Movimiento valido." ) ) { mostrarMensaje( "Movimiento valido, por favor espere.\n" ); establecerMarca( cuadroActual, miMarca ); // establece marca en el cuadro } // fin de if else if ( mensaje.equals( "Movimiento invalido, intente de nuevo" ) ) { mostrarMensaje( mensaje + "\n" ); // muestra el movimiento inválido miTurno = true; // sigue siendo turno de este cliente } // fin de else if else if ( mensaje.equals( "El oponente realizo movimiento" ) ) { int ubicacion = entrada.nextInt(); // obtiene la ubicación del movimiento entrada.nextLine(); // salta nueva línea después de la ubicación int int fila = ubicacion / 3; // calcula la fila int columna = ubicacion % 3; // calcula la columna establecerMarca( tablero[ fila ][ columna ], ( miMarca.equals( MARCA_X ) ? MARCA_O : MARCA_X ) ); // marca el movimiento mostrarMensaje( "El oponente hizo un movimiento. Ahora es su turno.\n" ); miTurno = true; // ahora es turno de este cliente } // fin de else if else mostrarMensaje( mensaje + "\n" ); // muestra el mensaje } // fin del método procesarMensaje // manipula el objeto areaSalida en el subproceso despachador de eventos private void mostrarMensaje( final String mensajeAMostrar ) { SwingUtilities.invokeLater( new Runnable() { public void run() { areaPantalla.append( mensajeAMostrar ); // actualiza la salida } // fin del método run

Figura 24.15 | Lado cliente del programa Tres en raya cliente/servidor. (Parte 3 de 5).

www.elsolucionario.net

24.8

168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224

Juego de Tres en raya (Gato) tipo cliente/servidor, utilizando un servidor con subprocesamiento... 1031

} // fin de la clase interna ); // fin de la llamada a SwingUtilities.invokeLater } // fin del método mostrarMensaje // método utilitario para establecer una marca en el tablero, en el subproceso despachador de eventos private void establecerMarca( final Cuadro cuadroAMarcar, final String marca ) { SwingUtilities.invokeLater( new Runnable() { public void run() { cuadroAMarcar.establecerMarca( marca ); // establece la marca en el cuadro } // fin del método run } // fin de la clase interna anónima ); // fin de la llamada a SwingUtilities.invokeLater } // fin del método establecerMarca // envía un mensaje al servidor, indicando el cuadro en el que se hizo clic public void enviarCuadroClic( int ubicacion ) { // si es mi turno if ( miTurno ) { salida.format( "%d\n", ubicacion ); // envía la ubicación al servidor salida.flush(); miTurno = false; // ya no es mi turno } // fin de if } // fin del método enviarCuadroClic // establece el cuadro actual public void establecerCuadroActual( Cuadro cuadro ) { cuadroActual = cuadro; // asigna el argumento al cuadro actual } // fin del método establecerCuadroActual // clase interna privada para los cuadros en el tablero private class Cuadro extends JPanel { private String marca; // marca a dibujar en este cuadro private int ubicacion; // ubicacion del cuadro public Cuadro( String marcaCuadro, int ubicacionCuadro ) { marca = marcaCuadro; // establece la marca para este cuadro ubicacion = ubicacionCuadro; // establece la ubicación de este cuadro addMouseListener( new MouseAdapter() { public void mouseReleased( MouseEvent e ) { establecerCuadroActual( Cuadro.this ); // establece el cuadro actual // envía la ubicación de este cuadro enviarCuadroClic( obtenerUbicacionCuadro() ); } // fin del método mouseReleased

Figura 24.15 | Lado cliente del programa Tres en raya cliente/servidor. (Parte 4 de 5).

www.elsolucionario.net

1032 Capítulo 24 Redes

225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263

} // fin de la clase interna anónima ); // fin de la llamada a addMouseListener } // fin del constructor de Cuadro // devuelve el tamaño preferido del objeto Cuadro public Dimension getPreferredSize() { return new Dimension( 30, 30 ); // devuelve el tamaño preferido } // fin del método getPreferredSize // devuelve el tamaño mínimo del objeto Cuadro public Dimension getMinimumSize() { return getPreferredSize(); // devuelve el tamaño preferido } // fin del método getMinimumSize // establece la marca para el objeto Cuadro public void establecerMarca( String nuevaMarca ) { marca = nuevaMarca; // establece la marca del cuadro repaint(); // vuelve a pintar el cuadro } // fin del método establecerMarca // devuelve la ubicación del objeto Cuadro public int obtenerUbicacionCuadro() { return ubicacion; // devuelve la ubicación del cuadro } // fin del método obtenerUbicacionCuadro // dibuja el objeto Cuadro public void paintComponent( Graphics g ) { super.paintComponent( g ); g.drawRect( 0, 0, 29, 29 ); // dibuja el cuadro g.drawString( marca, 11, 20 ); // dibuja la marca } // fin del método paintComponent } // fin de la clase interna Cuadro } // fin de la clase ClienteTresEnRaya

Figura 24.15 | Lado cliente del programa Tres en raya cliente/servidor. (Parte 5 de 5).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

// Fig. 24.16: PruebaClienteTresEnRaya.java // Prueba la clase ClienteTresEnRaya. import javax.swing.JFrame; public class PruebaClienteTresEnRaya { public static void main( String args[] ) { ClienteTresEnRaya aplicacion; // declara la aplicación cliente // si no hay argumentos de línea de comandos if ( args.length == 0 ) aplicacion = new ClienteTresEnRaya( "127.0.0.1" ); // localhost else aplicacion = new ClienteTresEnRaya( args[ 0 ] ); // usa args

Figura 24.16 | Clase de prueba para el cliente de Tres en raya. (Parte 1 de 2).

www.elsolucionario.net

24.8

17 18 19

Juego de Tres en raya (Gato) tipo cliente/servidor, utilizando un servidor con subprocesamiento... 1033

aplicacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); } // fin de main } // fin de la clase PruebaClienteTresEnRaya

Figura 24.16 | Clase de prueba para el cliente de Tres en raya. (Parte 2 de 2). Si el mensaje que se recibe es "Movimiento válido.", en las líneas 134 y 135 se muestra el mensaje "Movimiento válido, por favor espere." y se hace una llamada al método establecerMarca (líneas 173 a 184) para establecer la marca del cliente en el cuadro actual (el cuadro en el que el usuario hizo clic), utilizando el método invokeLater de SwingUtilities para asegurar que las actualizaciones de la GUI ocurran en el subproceso despachador de eventos. Si el mensaje recibido es "Movimiento inválido, intente de nuevo." en la línea 139 se muestra el mensaje para que el usuario pueda hacer clic en un cuadro distinto. Si el mensaje recibido es "El oponente realizo movimiento", en la línea 145 se lee un entero del servidor indicando a qué lugar se movió el oponente, y en las líneas 149 y 150 se coloca una marca en ese cuadro del tablero (de nuevo utilizando al método invokeLater de SwingUtilities, para asegurar que las actualizaciones en la GUI ocurran en el subproceso despachador de eventos). Si se recibe cualquier otro mensaje, en la línea 155 simplemente se muestra ese mensaje en pantalla. En la figura 24.17 se muestran capturas de pantalla de dos aplicaciones interactuando mediante el ServidorTresEnRaya como ejemplo.

Figura 24.17 | Pantallas de salida de ejemplo del programa Tres en raya cliente/servidor. (Parte 1 de 2).

www.elsolucionario.net

1034 Capítulo 24 Redes

Figura 24.17 | Pantallas de salida de ejemplo del programa Tres en raya cliente/servidor. (Parte 2 de 2).

24.9 La seguridad y la red A pesar de que sería muy interesante escribir una gran variedad de poderosas aplicaciones basadas en red, nuestros esfuerzos podrían estar limitados debido a cuestiones relacionadas con la seguridad. Muchos navegadores Web como Mozilla y Microsoft Internet Explorer prohíben, de manera predeterminada, que los applets de Java realicen un procesamiento de los archivos en los equipos en los que se ejecutan. Piense en ello. Un applet de Java está diseñado para ser enviado a su navegador a través de un documento HTML que puede descargarse de cualquier servidor Web en el mundo. Por lo general, es muy poco lo que podrá saber acerca de los orígenes de los applets de Java que se ejecuten en su sistema. Podría ser desastroso permitir que estos applets tuvieran la libertad de manipular sus archivos. Una situación más sutil ocurre al limitar los equipos con los que los applets en ejecución pueden realizar conexiones de red. Para crear aplicaciones con una verdadera capacidad de colaboración, sería ideal que nuestros applets pudieran comunicarse con equipos en casi cualquier parte. Por lo general, el administrador de seguridad de Java en un navegador Web restringe a un applet de manera que sólo pueda comunicarse con el equipo desde el que se descargó originalmente. Estas restricciones podrían parecer demasiado estrictas. Sin embargo, la API de seguridad de Java ahora proporciona herramientas para applets con firma digital, con lo cual los navegadores podrán determinar si un applet se descarga desde un origen de confianza (trusted source). Un applet de confianza puede recibir acceso adicional al equipo en el que se está ejecutando. Las características de la API de seguridad de Java y ciertas herramientas de red adicionales se describen en nuestro texto Advanced Java 2 Platform How to Program.

24.10 [Bono Web] Ejemplo práctico: servidor y cliente DeitelMessenger [Nota: este ejemplo práctico está disponible en www.deitel.com/books/jhtp7/]. Los salones de conversación se han vuelto bastante comunes en Internet. Estos salones proporcionan una ubicación central en donde los usuarios pueden conversar entre sí, mediante mensajes de texto cortos. Cada participante en un salón de conversación puede ver todos los mensajes que publican los demás usuarios, y cada uno de los usuarios puede publicar mensajes. Este ejemplo práctico integra muchas de las características de Java relacionadas con redes, subprocesamiento múltiple y la GUI de Swing que hemos aprendido hasta ahora, para crear un sistema de conversación en línea. También presentaremos la transmisión múltiple (multicasting), que permite a una aplicación enviar objetos DatagramPacket a grupos de clientes. El ejemplo práctico DeitelMessenger es una aplicación considerable que utiliza muchas características intermedias de Java, como el trabajo en red con objetos Socket, DatagramPacket y MulticastSocket, subprocesamiento múltiple y la GUI de Swing. En este ejemplo práctico también se demuestran las buenas prácticas de ingeniería de software al separar la interfaz de la implementación, lo cual permite a los desarrolladores dar soporte a distintos protocolos de red, y proporcionar distintas interfaces de usuario. Después de leer este ejemplo práctico, usted podrá construir aplicaciones de red más importantes.

www.elsolucionario.net

Resumen 1035

24.11 Conclusión En este capítulo aprendió acerca de los fundamentos de la programación de redes en Java. Conoció dos métodos distintos para enviar datos a través de una red: el trabajo en red basado en flujos mediante el uso de TCP/IP, y el trabajo en red basado en datagramas mediante el uso de UDP. En el siguiente capítulo, aprenderá acerca de los conceptos básicos de las bases de datos, a interactuar con los datos en una base de datos mediante el uso de SQL, y a utilizar JDBC para permitir que las aplicaciones de Java manipulen los datos de una base de datos.

Resumen Sección 24.1 Introducción • Java cuenta con sockets de flujo y sockets de datagrama. Con los sockets de flujo, un proceso establece una conexión con otro proceso. Mientras la conexión esté presente, los datos fluyen entre los procesos en flujos. Se dice que los sockets de flujo proporcionan un servicio orientado a la conexión. El protocolo que se utiliza para la transmisión es el popular TCP (Protocolo de control de transmisión). • Con los sockets de datagrama, se transmiten paquetes individuales de información. UDP (Protocolo de datagramas de usuario) es un servicio sin conexión que no garantiza que los paquetes no se pierdan, se dupliquen o lleguen fuera de secuencia. Se requiere un esfuerzo de programación adicional de parte del programador para lidiar con estos problemas.

Sección 24.2 Manipulación de URLs • El protocolo HTTP (Protocolo de transferencia de hipertexto) que forma la base del servicio Web utiliza URIs (Identificadores uniformes de recursos) para localizar datos en Internet. Los URIs comunes representan archivos o directorios, y pueden representar tareas complejas como búsquedas en bases de datos y en Internet. Un URI que representa a un documento se conoce como URL (Identificador uniforme de recursos). • El método getAppletContext de Applet devuelve una referencia a un objeto AppletContext, el cual representa el entorno del applet (es decir, el navegador en el que se ejecuta el applet). El método showDocument de AppletContext recibe un URL como argumento y lo pasa al objeto AppletContext (es decir, el navegador), el cual muestra el recurso Web asociado con ese URL. Una segunda versión de showDocument permite a un applet especificar el marco de destino en el que se va a visualizar un recurso Web. Los marcos de destino especiales son: _blank (se muestra en una nueva ventana del navegador Web), _self (se muestra en el mismo marco que el applet) y _top (se eliminan los marcos actuales y después se muestra en la ventana actual).

Sección 24.3 Cómo leer un archivo en un servidor Web • El método setPage de JEditorPane descarga el documento especificado por su argumento y lo muestra en el objeto JEditorPane. • Por lo general, un documento HTML contiene hipervínculos (texto, imágenes o componentes de la GUI que, cuando se hace clic en ellos, proporcionan un acceso rápido a otro documento en Web. Si un documento HTML se muestra en un objeto JEditorPane y el usuario hace clic en un hipervínculo, el objeto JEditorPane genera un evento HyperlinkEvent y notifica a todos los objetos HyperlinkListener acerca de ese evento. • El método getEventType de HyperlinkEvent determina el tipo del evento. HyperlinkEvent contiene la clase anidada EventType, la cual declara tres tipos de eventos de hipervínculo: ACTIVATED (se hizo clic en el hipervínculo), ENTERED (se desplazó el ratón sobre un hipervínculo) y EXITED (se retiró el ratón de un hipervínculo). El método getURL de HyperlinkEvent obtiene el URL representado por el hipervínculo.

Sección 24.4 Cómo establecer un servidor simple utilizando sockets de flujo • Las conexiones basadas en flujo se administran con objetos Socket. • Un objeto ServerSocket establece el puerto en el que el servidor espera las conexiones de los clientes. El segundo argumento para el constructor de ServerSocket es el número de conexiones que pueden esperar en una cola para conectarse con el servidor. Si la cola de clientes está llena, se rechazan las conexiones de los clientes. El método accept de ServerSocket espera de manera indefinida (es decir, se bloquea) una conexión de un cliente y devuelve un objeto Socket cuando se establece una conexión. • Los métodos getOutputStream y getInputStream de Socket obtienen referencias a objetos OutputStream e InputStream de un objeto Socket, respectivamente. El método close de Socket termina una conexión.

www.elsolucionario.net

1036 Capítulo 24 Redes

Sección 24.5 Cómo establecer un cliente simple utilizando sockets de flujo • Al crear un objeto Socket se especifica un nombre de servidor y un número de puerto, para que éste pueda conectar a un cliente con el servidor. Un intento de conexión fallido lanza una excepción IOException. • El método getByName de InetAddress devuelve un objeto InetAddress que contiene el nombre de host de la computadora para la cual se especifica el nombre de host o dirección IP como argumento. El método getLocalHost de InetAddress devuelve un objeto InetAddress que contiene el nombre de host del equipo local que ejecuta el programa.

Sección 24.7 Interacción entre cliente/servidor sin conexión mediante datagramas • La transmisión orientada a la conexión es como el sistema telefónico: usted marca y recibe una conexión al teléfono de la persona con la que usted desea comunicarse. La conexión se mantiene todo el tiempo que dure su llamada telefónica, incluso aunque usted no esté hablando. • La transmisión sin conexión mediante datagramas es similar a la manera en que el correo se transporta mediante el servicio postal. Si un mensaje extenso no cabe en un sobre, se puede dividir en varias piezas separadas que se colocan en sobres separados, numerados en forma secuencial. Cada una de las cartas se envía entonces por correo al mismo tiempo. Las cartas podrían llegar en orden, sin orden o tal vez no llegarían. • Los objetos DatagramPacket almacenan paquetes de datos que una aplicación va a enviar o recibir. Los objetos DatagramSocket envían y reciben objetos DatagramPacket. • El constructor de DatagramSocket que no recibe argumentos enlaza la aplicación a un puerto elegido por la computadora en la que se ejecuta el programa. El constructor de DatagramSocket que recibe un número de puerto entero enlaza la aplicación al puerto especificado. Si un constructor de DatagramSocket no enlaza la aplicación a un puerto, se produce una excepción SocketException. El método receive de DatagramSocket se bloquea (espera) hasta que llega un paquete, y después almacena este paquete en su argumento. • El método getAddress de DatagramPacket devuelve un objeto InetAddress que contiene información acerca del equipo host desde el que se envió el paquete. El método getPort devuelve un entero que especifica el número de puerto a través del cual el equipo host envió el objeto DatagramPacket. El método getLength devuelve un entero que representa el número de bytes de datos en un objeto DatagramPacket. El método getData devuelve un arreglo de bytes que contiene los datos en un objeto DatagramPacket. • El constructor de DatagramPacket para un paquete que se va a enviar recibe cuatro argumentos: el arreglo de bytes que se va a enviar, el número de bytes a enviar, la dirección cliente a la que se enviará el paquete y el número de puerto en el que espera el cliente para recibir paquetes. • El método send de DatagramSocket envía un objeto DatagramPacket a través de la red. • Si ocurre un error al recibir o enviar un objeto DatagramPacket, se produce una excepción IOException.

Sección 24.9 La seguridad y la red • Por lo general, los navegadores Web restringen a un applet, de manera que sólo pueda comunicarse con el equipo desde el que se descargó originalmente.

Terminología _blank, marco de destino _self, marco de destino _top, marco de destino accept, método de la clase ServerSocket ACTIVATED, constante de la clase anidada EventType AppletContext, interfaz

bloquear para escuchar en espera de una conexión cargador de clases cliente close, método de la clase Socket comunicación basada en sockets comunicaciones basadas en paquetes conexión Consorcio World Wide Web (W3C) DatagramPacket, clase DatagramSocket, clase dirección de bucle local (loopback)

encabezado de flujo enlazar el servidor al puerto ENTERED, constante de la clase anidada EventType EventType, clase anidada de HyperlinkEvent EXITED, constante de la clase anidada EventType getAddress, método de la clase DatagramPacket getAppletContext, método de la clase Applet getByName, método de la clase InetAddress getData, método de la clase DatagramPacket getEventType, método de la clase HyperlinkEvent getHostName, método de la clase InetAddress getInetAddress, método de la clase Socket getInputStream, método de la clase Socket getLength, método de la clase DatagramPacket getLocalHost, método de la clase InetAddress getOutputStream, método de la clase Socket getParameter, método de la clase Applet

www.elsolucionario.net

Ejercicios de autoevaluación 1037 getPort, método de la clase DatagramPacket getResource, método de la clase Class getURL, método de la clase HyperlinkEvent HyperlinkEvent, clase HyperlinkListener, interfaz hyperlinkUpdate, método de la interfaz HyperlinkListener

Identificador uniforme de recursos (URI) InetAddress, clase java.net, paquete JEditorPane, clase longitud de cola MalformedURLException, clase marco de destino marco de HTML número de puerto origen de confianza paquete paquete de datagrama param, etiqueta parámetro de applet Protocolo de datagramas de usuario (UDP) Protocolo de transferencia de hipertexto (HTTP) puerto

punto de negociación (handshake) método de la clase DatagramSocket registrar un puerto relación cliente/servidor repite un paquete y lo envía de vuelta al cliente send, método de la clase DatagramSocket ServerSocket, clase servicio orientado a la conexión servicio sin conexión servidor setPage, método de la clase JEditorPane showDocument, método de la clase AppletContext socket socket de datagrama socket de flujo Socket, clase SocketException, clase TCP (Protocolo de control de transmisión) transmisión basada en flujos, orientada a la conexión transmisión basada en sockets transmisión sin conexión UDP (Protocolo de datagramas de usuario) UnknownHostException, clase receive,

Ejercicios de autoevaluación 24.1

Complete las siguientes oraciones: a) La excepción _______________ ocurre cuando se produce un error de entrada/salida al cerrar un socket. b) La excepción _______________ ocurre cuando un nombre de host indicado por un cliente no se puede resolver a una dirección. c) Si un constructor de DatagramSocket no puede establecer un objeto DatagramSocket apropiadamente, se produce una excepción del tipo _______________. d) Muchas de las clases para trabajo en red de Java están contenidas en el paquete _______________. e) La clase _______________ enlaza la aplicación a un puerto para la transmisión de datagramas. f ) Un objeto de la clase _______________ contiene una dirección IP. g) Los dos tipos de sockets que vimos en este capítulo son _______________ y _______________. h) El acrónimo URL significa _______________. i) El acrónimo URI significa _______________. j) El protocolo clave que forma la base de la World Wide Web es _______________. k) El método _______________ de AppletContext recibe un objeto URL como argumento y muestra en un navegador el recurso Web asociado con ese URL. l) El método getLocalHost devuelve un objeto _______________ que contiene el nombre de host local de la computadora en la que se está ejecutando el programa. m) El constructor de URL determina si su argumento de cadena es un URL válido. De ser así, el objeto URL se inicializa con esa ubicación; en caso contrario, se produce una excepción _______________.

24.2

Conteste con verdadero o falso a cada una de las siguientes proposiciones; en caso de ser falso, explique por qué a) UDP es un protocolo orientado a la conexión. b) Con los sockets de flujo, un proceso establece una conexión con otro proceso. c) Un servidor espera en un puerto las conexiones de un cliente. d) La transmisión de paquetes con datagramas a través de una red es un proceso confiable; se garantiza que los paquetes lleguen en secuencia.

www.elsolucionario.net

1038 Capítulo 24 Redes e) Por razones de seguridad, muchos navegadores Web como Mozilla permiten a los applet de Java realizar el procesamiento de archivos solamente en los equipos en los que se ejecutan. f ) A menudo, los navegadores Web restringen a un applet de manera que sólo pueda comunicarse con el equipo desde el que se descargó originalmente.

Respuestas a los ejercicios de autoevaluación 24.1 a) IOException. b) UnknownHostException. c) SocketException. d) java.net. e) DatagramSocket. f ) InetAddress. g) sockets de flujo, sockets de datagrama. h) Localizador uniforme de recursos. i) Identificador uniforme de recursos. j) http. k) showDocument. l) InetAddress. m) MalformedURLException. 24.2 a) Falso; UDP es un protocolo sin conexión y TCP es un protocolo orientado a la conexión. b) Verdadero. c) Verdadero. d) Falso; los paquetes podrían perderse, llegar desordenados o duplicarse. e) Falso; la mayoría de los navegadores evita que los applets realicen un procesamiento de archivos en el equipo cliente. f ) Verdadero.

Ejercicios 24.3

Indique la diferencia entre los servicios de red orientados a la conexión y los servicios sin conexión.

24.4

¿Cómo determina un cliente el nombre de host del equipo cliente?

24.5

¿Bajo qué circunstancias se lanzaría una excepción SocketException?

24.6

¿Cómo puede un cliente obtener una línea de texto de un servidor?

24.7

Describa cómo se conecta un cliente con un servidor.

24.8

Describa cómo un servidor envía datos a un cliente.

24.9

Describa cómo operar un servidor para recibir una petición de conexión basada en flujo de un solo cliente.

24.10 ¿Cómo escucha un servidor las conexiones de sockets basadas en flujo en un puerto? 24.11 ¿Qué es lo que determina cuántas peticiones de conexión de los clientes pueden esperar en una cola para conectarse con un servidor? 24.12 Según lo descrito en el texto, ¿cuáles son las razones que podrían ocasionar que un servidor rechazara una petición de conexión de un cliente? 24.13 Use una conexión de socket para permitir a un cliente especificar un nombre de archivo, y haga que el servidor envíe el contenido del mismo o indique que el archivo no existe. 24.14 Modifique el ejercicio 24.13 para permitir al cliente modificar el contenido del archivo y enviarlo de vuelta al servidor para que lo almacene. El usuario puede editar el archivo en un objeto JTextArea y después hacer clic en un botón guardar cambios para enviar el archivo de vuelta al servidor. 24.15 Modifique el programa de la figura 24.2 para permitir que los usuarios agreguen sus propios sitios a la lista, y que también los eliminen de la lista. 24.16 Los servidores con subprocesamiento múltiple son bastante populares hoy en día, especialmente debido al uso cada vez mayor de servidores con multiprocesamiento. Modifique la aplicación de servidor simple presentada en la sección 24.6, para que sea un servidor con subprocesamiento múltiple. Después utilice varias aplicaciones cliente y haga que cada una de ellas se conecte al servidor en forma simultánea. Use un objeto ArrayList para almacenar los subprocesos cliente. ArrayList proporciona varios métodos que pueden utilizarse en este ejercicio. El método size determina el número de elementos en un objeto ArrayList. El método get devuelve el elemento en la ubicación especificada por su argumento. El método add coloca su argumento al final del objeto ArrayList. El método remove elimina su argumento del objeto ArrayList. 24.17 (Juego de damas) En el texto presentamos un programa de Tres en raya, controlado por un servidor con subprocesamiento múltiple. Desarrolle un programa de damas que se modele en base al programa de Tres en raya. Los dos usuarios deberán alternar sus movimientos. Su programa debe mediar los movimientos de los jugadores, determinando el turno de cada quién y permitiendo sólo movimientos válidos. Los mismos jugadores determinarán cuando el juego se haya acabado. 24.18 (Juego de ajedrez) Desarrolle un programa para jugar ajedrez, modelado en base al programa de damas del ejercicio 24.17.

www.elsolucionario.net

Ejercicios 1039 24.19 (Juego de Blackjack) Desarrolle un programa para jugar cartas estilo Blackjack, en el que la aplicación servidor reparta las cartas a cada uno de los applets cliente. El servidor deberá repartir cartas adicionales (según las reglas del juego) a cada jugador, según sea requerido. 24.20 (Juego de Póquer) Desarrolle un programa para jugar cartas estilo Póquer, en el que la aplicación servidor reparta las cartas a cada uno de los applets cliente. El servidor deberá repartir cartas adicionales (según las reglas del juego) a cada jugador, según sea requerido. 24.21 (Modificaciones al programa de Tres en raya con subprocesamiento múltiple) Los programas de las figuras 24.13 y 24.15 implementan una versión cliente/servidor con subprocesamiento múltiple del juego Tres en raya. Nuestro objetivo al desarrollar este juego fue demostrar el uso de un servidor con subprocesamiento múltiple que pudiera procesar varias conexiones de clientes al mismo tiempo. El servidor en el ejemplo realmente es un mediador entre los dos applets cliente; se asegura que cada movimiento sea válido y que cada cliente se mueva en el orden apropiado. El servidor no determina quién ganó o perdió, o si hubo un empate. Además, no existe la capacidad de permitir que se inicie un juego nuevo, o de terminar un juego existente. A continuación se muestra una lista de modificaciones sugeridas a las figuras 24.13 y 24.15: a) Modificar la clase ServidorTresEnRaya para probar si se ganó, perdió o empató en cada movimiento del juego. Enviar un mensaje a cada applet cliente que indique el resultado del juego cuando éste termine. b) Modificar la clase ClienteTresEnRaya para mostrar un botón que, al hacer clic sobre él, permita al cliente jugar otro juego. El botón deberá habilitarse sólo cuando se complete un juego. Observe que las clases ClienteTresEnRaya y ServidorTresEnRaya deben modificarse para restablecer el tablero y toda la demás información de estado. Además, el otro ClienteTresEnRaya debe ser notificado cuando un nuevo juego esté por empezar, para que su tablero y su estado puedan restablecerse. c) Modificar la clase ClienteTresEnRaya para mostrar un botón que, al hacer clic sobre él, el cliente pueda terminar el programa en cualquier momento. Cuando el usuario haga clic en el botón, el servidor y el otro cliente deberán ser notificados. Después el servidor deberá esperar una conexión de otro cliente, para que pueda empezar un juego nuevo. d) Modifique las clases ClienteTresEnRaya y ServidorTresEnRaya de manera que el ganador de un juego pueda seleccionar la pieza X u O para el siguiente juego. Recuerde: la X siempre va primero. e) Si le gusta ser ambicioso, permita a un cliente jugar contra el servidor mientras éste espera la conexión de otro cliente. 24.22 (Tres en raya con subprocesamiento múltiple, en 3-D) Modifique el programa de Tres en raya cliente/servidor con subprocesamiento múltiple, para implementar una versión tridimensional del juego, de 4 × 4 × 4. Implemente la aplicación servidor para que sea el intermediario entre los dos clientes. Muestre el tablero tridimensional como cuatro tableros que contienen cuatro filas y cuatro columnas cada uno. Si le gusta ser ambicioso, trate de hacer las siguientes modificaciones: a) Dibuje el tablero en forma tridimensional. b) Permita al servidor probar si hay un ganador, un perdedor o si fue empate. ¡Cuidado! ¡Hay muchas posibles maneras de ganar en un tablero de 4 × 4 × 4! 24.23 (Código Morse en red) Tal vez el más famoso de todos los esquemas de codificación sea 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 cortos a cada letra del alfabeto, a cada dígito y a unos cuantos caracteres especiales (punto, coma, signo de dos puntos y signo de punto y coma). En los sistemas orientados al sonido, el punto representa un sonido corto y el guión corto representa un sonido largo. En los sistemas orientados a la luz y en los sistemas de señalización con banderas se utilizan otras representaciones de puntos y guiones cortos. La separación entre las palabras se indica mediante un espacio o, simplemente, la ausencia de un punto o un guión corto. En un sistema orientado al sonido, un espacio se indica mediante un lapso corto de tiempo durante el cual no se transmite ningún sonido. La versión internacional del código Morse aparece en la figura 24.18. Escriba una aplicación cliente/servidor en la que dos clientes puedan enviarse entre sí mensajes en código Morse, a través de una aplicación servidor con subprocesamiento múltiple. La aplicación cliente deberá permitir al usuario escribir caracteres normales en un objeto JTextArea. Cuando el usuario envíe el mensaje, la aplicación cliente deberá traducir los caracteres en código Morse y enviar el mensaje codificado a través del servidor, al otro cliente. Use un espacio en blanco entre cada letra en código Morse y tres espacios en blanco entre cada palabra en código Morse. Cuando se reciban los mensajes, deberán ser decodificados y mostrados como caracteres normales y como código Morse. El cliente debe tener un objeto JTextArea para escribir y un objeto JTextArea para mostrar los mensajes del otro cliente.

www.elsolucionario.net

1040 Capítulo 24 Redes

Carácter

Código

Carácter

Código

A

.-

T

-

B

-...

U

..-

C

-.-.

V

...-

D

-..

W

.--

E

.

X

-..-

F

..-.

Y

-.--

G

--.

Z

--..

H

....

I

..

Dígitos

J

.---

1

.----

K

-.-

2

..---

L

.-..

3

...--

M

--

4

....-

N

-.

5

.....

O

---

6

-....

P

.--.

7

--...

Q

--.-

8

---..

R

.-.

9

----.

S

...

0

-----

Figura 24.18 | Las letras del alfabeto, expresadas en código Morse internacional.

www.elsolucionario.net

25 Es un error capital especular antes de tener datos.

Acceso a bases de datos con JDBC

—Arthur Conan Doyle

Ahora ven, escríbelo en una tablilla, grábalo en un libro, y que dure hasta el último día, para testimonio.

OBJETIVOS

—La Santa Biblia, Isaías 30:8

En este capítulo aprenderá a:

Primero consiga los hechos, y después podrá distorsionarlos según le parezca. —Mark Twain

Me gustan dos tipos de hombres: domésticos y foráneos.

Q

Utilizar los conceptos acerca de las bases de datos relacionales.

Q

Utilizar el Lenguaje de Consulta Estructurado (SQL) para obtener y manipular los datos de una base de datos.

Q

Utilizar la API JDBC™ del paquete java.sql para acceder a las bases de datos.

Q

Utilizar la interfaz RowSet del paquete javax.sql para manipular bases de datos.

Q

Utilizar el descubrimiento de controladores de JDBC automático de JDBC 4.0.

Q

Utilizar objetos PreparedStatement para crear instrucciones de SQL precompiladas con parámetros.

Q

Conocer cómo el procesamiento de transacciones hace que las aplicaciones de datos sea más robusto.

—Mae West

www.elsolucionario.net

Pla n g e ne r a l

1042 Capítulo 25

25.1 25.2 25.3 25.4

25.5 25.6 25.7 25.8

25.9 25.10 25.11 25.12 25.13 25.14 25.15

Acceso a bases de datos con JDBC

Introducción Bases de datos relacionales Generalidades acerca de las bases de datos relacionales: la base de datos libros SQL 25.4.1 Consulta básica SELECT 25.4.2 La cláusula WHERE 25.4.3 La cláusula ORDER BY 25.4.4 Cómo fusionar datos de varias tablas: INNER JOIN 25.4.5 La instrucción INSERT 25.4.6 La instrucción UPDATE 25.4.7 La instrucción DELETE Instrucciones para instalar MySQL y MySQL Connector/J Instrucciones para establecer una cuenta de usuario de MySQL Creación de la base de datos libros en MySQL Manipulación de bases de datos con JDBC 25.8.1 Cómo conectarse y realizar consultas en una base de datos 25.8.2 Consultas en la base de datos libros La interfaz RowSet Java DB/Apache Derby Objetos PreparedStatement Procedimientos almacenados Procesamiento de transacciones Conclusión Recursos Web y lecturas recomendadas

Resumen | Terminología | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

25.1 Introducción Una base de datos es una colección organizada de datos. Existen diversas estrategias para organizar datos y facilitar el acceso y la manipulación. Un sistema de administración de bases de datos (DBMS) proporciona los mecanismos para almacenar, organizar, obtener y modificar datos para muchos usuarios. Los sistemas de administración de bases de datos permiten el acceso y almacenamiento de datos sin necesidad de preocuparse por su representación interna. En la actualidad, los sistemas de bases de datos más populares son las bases de datos relacionales, en donde los datos se almacenan sin considerar su estructura física (sección 25.2). Un lenguaje llamado SQL es el lenguaje estándar internacional que se utiliza casi universalmente con las bases de datos relacionales para realizar consultas (es decir, para solicitar información que satisfaga ciertos criterios) y para manipular datos. Algunos sistemas de administración de bases de datos relacionales (RDBMS) populares son Microsoft SQL Server, Oracle, Sybase, IBM DB2, Informix, PostgreSQL y MySQL. El JDK incluye ahora un RDBMS puro de Java, conocido como Java DB (la versión de Sun de Apache Derby). En este capítulo presentaremos ejemplos utilizando MySQL y Java DB.1

1.

MySQL es uno de los sistemas de administración de bases de datos de código fuente abierto más populares de la actualidad. Al momento de escribir este libro todavía no soportaba JDBC 4, que forma parte de Java SE 6 (Mustang). Sin embargo, el sistema Java DB de Sun, que está basado en el sistema de administración de bases de datos de código fuente abierto Apache Derby y se incluye con el JDK 1.6.0 de Sun, sí ofrece soporte para JDSBC 4. En las secciones 25.8 a 25.10 utilizamos MySQL y JDBC 3, y en la sección 25.11 utilizamos Java DB y JDBC 4.

www.elsolucionario.net

25.2

Bases de datos relacionales 1043

Los programas en Java se comunican con las bases de datos y manipulan sus datos utilizando la API JDBC™. Un controlador de JDBC permite a las aplicaciones de Java conectarse a una base de datos en un DBMS específico, y nos permite manipular esa base de datos mediante la API JDBC.

Observación de ingeniería de software 25.1 Al utilizar la API JDBC, los desarrolladores pueden modificar el DBMS subyacente sin necesidad de modificar el código de Java que accede a la base de datos.

La mayoría de los sistemas de administración de bases de datos populares incluyen ahora controladores de JDBC. También hay muchos controladores de JDBC de terceros disponibles. En este capítulo presentaremos la tecnología JDBC y la emplearemos para manipular bases de datos de MySQL y Java DB. Las técnicas que demostraremos aquí también pueden usarse para manipular otras bases de datos que tengan controladores de JDBC. Consulte la documentación de su DBMS para determinar si incluye un controlador de JDBC. Incluso si su DBMS no viene con un controlador de JDBC, muchos distribuidores independientes proporcionan estos controladores para una amplia variedad de sistemas DBMS. Para obtener más información acerca de JDBC, visite la página: java.sun.com/javase/technologies/database/index.jsp

Este sitio contiene información relacionada con JDBC, incluyendo las especificaciones de JDBC, preguntas frecuentes (FAQs) acerca de JDBC, un centro de recursos de aprendizaje y descargas de software para buscar controladores de JDBC para su DBMS, developers.sun.com/product/jdbc/drivers/

Este sitio proporciona un motor de búsqueda para ayudarlo a localizar los controladores apropiados para su DBMS.

25.2 Bases de datos relacionales Una base de datos relacional es una representación lógica de datos que permite acceder a éstos sin necesidad de considerar su estructura física. Una base de datos relacional almacena los datos en tablas. En la figura 25.1 se muestra una tabla de ejemplo que podría utilizarse en un sistema de personal. El nombre de la tabla es Empleado, y su principal propósito es almacenar los atributos de un empleado. Las tablas están compuestas de filas, y las filas, de columnas en las que se almacenan los valores. Esta tabla consiste de seis filas. La columna Numero de cada fila en esta tabla es su clave primaria: una columna (o grupo de columnas) en una tabla que tiene un valor único, el cual no puede duplicarse en las demás filas. Esto garantiza que cada fila pueda identificarse por su clave primaria. Algunos buenos ejemplos de columnas con clave primaria son un número de seguro social, un número de identificación de empleado y un número de pieza en un sistema de inventario, ya que se garantiza que los valores en cada una de esas columnas serán únicos. Las filas de la figura 25.1 se muestran en orden, con base en la clave primaria. En este caso, las filas se muestran en orden ascendente; también podríamos utilizar el orden descendente.

Fila

Número

Nombre

Departamento

Salario

Ubicación

23603

Jones

413

1100

New Jersey

24568

Kerwin

413

2000

New Jersey

34589

Larson

642

1800

Los Angeles

35761

Myers

611

1400

Orlando

47132

Neumann

413

9000

New Jersey

78321

Stephens

611

8500

Orlando

Clave primaria

Columna

Figura 25.1 | Datos de ejemplo de la tabla Empleado.

www.elsolucionario.net

1044 Capítulo 25

Acceso a bases de datos con JDBC

No se garantiza que las filas en las tablas se almacenen en un orden específico. Como lo demostraremos en un ejemplo más adelante, los programas pueden especificar criterios de ordenamiento al solicitar datos de una base de datos. Cada columna de la tabla representa un atributo de datos distinto. Las filas generalmente son únicas (por clave primaria) dentro de una tabla, pero los valores de columnas específicas pueden duplicarse entre filas. Por ejemplo, tres filas distintas en la columna Departamento de la tabla Empleado contienen el número 413. A menudo los distintos usuarios de una base de datos se interesan en datos diferentes, y en relaciones distintas entre esos datos. La mayoría de los usuarios requieren solamente de ciertos subconjuntos de las filas y columnas. Para obtener estos subconjuntos, utilizamos consultas para especificar cuáles datos se deben seleccionar de una tabla. Utilizamos SQL para definir consultas complejas que seleccionen datos de una tabla. Por ejemplo, podríamos seleccionar datos de la tabla Empleado para crear un resultado que muestre en dónde se ubican los departamentos, y presentar los datos ordenados en forma ascendente, por número de departamento. Este resultado se muestra en la figura 25.2. Hablaremos sobre las consultas de SQL en la sección 25.4.

Departamento

Ubicación

413 611 642

New Jersey Orlando Los Angeles

Figura 25.2 | Resultado de seleccionar distintos datos de Departamento y Ubicacion de la tabla Empleado.

25.3 Generalidades acerca de las bases de datos relacionales: la base de datos libros

Ahora veremos las generalidades sobre las bases de datos relacionales, y para ello emplearemos una base de datos llamada libros, misma que creamos para este capítulo. Antes de hablar sobre SQL, veremos una descripción general de las tablas de la base de datos libros. Utilizaremos esta base de datos para presentar varios conceptos de bases de datos, incluyendo el uso de SQL para obtener información de la base de datos y manipular los datos. Le proporcionaremos una secuencia de comandos (script) para crear la base de datos. En el directorio de ejemplos para este capítulo (en el CD que acompaña al libro) encontrará la secuencia de comandos. En la sección 25.5 le explicaremos cómo utilizar esta secuencia de comandos. La base de datos consiste de tres tablas: autores, isbnAutor y titulos. La tabla autores (descrita en la figura 25.3) consta de tres columnas que mantienen el número único de identificación de cada autor, su nombre de pila y apellido. La figura 25.4 contiene datos de ejemplo de la tabla autores de la base de datos libros. La tabla isbnAutor (descrita en la figura 25.5) consta de dos columnas que representan a cada ISBN y el correspondiente número de ID del autor. Esta tabla asocia a los autores con sus libros. Ambas columnas son claves externas que representan la relación entre las tablas autores y titulos; una fila en la tabla autores puede estar asociada con muchas filas en la tabla titulos y viceversa. La figura 25.6 contiene datos de ejemplo de la tabla isbnAutor de la base de datos libros. [Nota: para ahorrar espacio, dividimos el contenido de esta tabla en

Columna

Descripción

idAutor

El número de identificación (ID) del autor en la base de datos. En la base de datos libros, esta columna de enteros se define como autoincrementada; para cada fila insertada en esta tabla, la base de datos incrementa automáticamente el valor de idAutor por 1 para asegurar que cada fila tenga un idAutor único. Esta columna representa la clave primaria de la tabla.

nombrePila

El nombre de pila del autor (una cadena).

apellidoPaterno

El apellido paterno del autor (una cadena).

Figura 25.3 | La tabla autores de la base de datos libros.

www.elsolucionario.net

25.3

Generalidades acerca de las bases de datos relacionales: la base de datos libros 1045

idAutor

nombrePila

apellidoPaterno

1

Harvey

Deitel

2

Paul

Deitel

3

Tem

Nieto

4

Sean

Santry

Figura 25.4 | Datos de ejemplo de la tabla autores.

Columna

Descripción

idAutor

El número de identificación (ID) del autor, una clave externa para la tabla autores.

isbn

El ISBN para un libro, una clave externa para la tabla titulos.

Figura 25.5 | La tabla isbnAutor de la base de datos libros.

idAutor

isbn

idAutor

isbn

1

0131869000

2

0131450913

2

0131869000

1

0131828274

1

0131483986

2

0131828274

2

0131483986

3

0131450913

1

0131450913

4

0131828274

Figura 25.6 | Datos de ejemplo de la tabla isbnAutor de libros. dos columnas, cada una de las cuales contiene las columnas idAutor y isbn.] La columna idAutor es una clave externa; una columna en esta tabla que coincide con la columna de la clave primaria en otra tabla (por ejemplo, idAutor en la tabla autores). Las claves externas se especifican al crear una tabla. La clave externa ayuda a mantener la Regla de integridad referencial: todo valor de una clave externa debe aparecer como el valor de la clave primaria de otra tabla. Esto permite al DBMS determinar si el valor de idAutor para un libro específico es válido. Las claves externas también permiten seleccionar datos relacionados en varias tablas para fines analíticos; a esto se conoce como unir los datos. La tabla titulos descrita en la figura 25.7 consiste en cuatro columnas que representan el ISBN, el título, el número de edición y el año de copyright. La tabla está en la figura 25.8. Hay una relación de uno a varios entre una clave primaria y su correspondiente clave externa (por ejemplo, una editorial puede publicar muchos libros). Una clave externa puede aparecer varias veces en su propia tabla, pero sólo una vez (como clave primaria) en otra tabla. La figura 25.9 es un diagrama de relación de entidades (ER) para la base de datos libros. Este diagrama muestra las tablas en la base de datos, así como las relaciones entre ellas. El primer compartimiento en cada cuadro contiene el nombre de la tabla. Los nombres en cursiva son claves primarias. La clave primaria de una tabla identifica en forma única a cada fila. Cada fila debe tener un valor en la clave primaria, y éste debe ser único en la tabla. A esto se le conoce como Regla de integridad de entidades.

Error común de programación 25.1 Si no se proporciona un valor para cada columna en una clave primaria, se quebranta la Regla de Integridad de Entidades y el DBMS reporta un error.

www.elsolucionario.net

1046 Capítulo 25

Acceso a bases de datos con JDBC

Columna

Descripción

isbn

El número ISBN del libro (una cadena). La clave primaria de la tabla. ISBN son las siglas de “International Standard Book Number” (Número internacional normalizado para libros); un esquema de numeración utilizado por las editoriales en todo el mundo para dar a cada libro un número de identificación único.

titulo

Título del libro (una cadena).

numeroEdicion

Número de edición del libro (un entero).

copyright

Año de edición (copyright) del libro (una cadena).

Figura 25.7 | La tabla titulos de la base de datos libros.

isbn

titulo

numeroEdicion

copyright

0131869000

Visual Basic How to Program

3

2006

0131525239

Visual C# How to Program

2

2006

0132222205

Java How to Program

7

2007

0131857576

C++ How to Program

5

2005

0132404168

C How to Program

5

2007

0131450913

Internet & World Wide Web How to Program

3

2004

Figura 25.8 | Datos de ejemplo para la tabla titulos de la base de datos libros.

Autores IDAutor NombrePila

Titulos

ISBNAutor 1

IDAutor ISBN

1

ISBN Titulo NumeroEdicion

Apellido Paterno

Copyright

Figura 25.9 | Relaciones de las tablas en la base de datos libros.

Error común de programación 25.2 Al proporcionar el mismo valor para la clave primaria en varias filas, el DBMS reporta un error.

Las líneas que conectan las tablas en la figura 25.9 representan las relaciones entre las tablas. Considere la línea entre las tablas isbnAutor y autores. En el extremo de la línea que va a autores hay un 1, y en el extremo que va a isbnAutor hay un símbolo de infinito (∞), el cual indica una relación de uno a varios en la que cualquier autor de la tabla autores puede tener un número arbitrario de libros en la tabla isbnAutor. Observe que la línea de relación enlaza a la columna idAutor en la tabla autores (su clave primaria) con la columna idAutor en la tabla isbnAutor (es decir, su clave externa). La columna idAutor en la tabla isbnAutor es una clave externa.

www.elsolucionario.net

25.4

SQL 1047

Error común de programación 25.3 Al proporcionar un valor de clave externa que no aparezca como valor de clave primaria en otra tabla, se quebranta la Regla de Integridad Referencial y el DBMS reporta un error.

La línea entre las tablas titulos e isbnAutor muestra otra relación de uno a varios; un título puede ser escrito por cualquier número de autores. De hecho, el único propósito de la tabla isbnAutor es proporcionar una relación de varios a varios entre las tablas autores y titulos; un autor puede escribir cualquier número de libros y un libro puede tener cualquier número de autores.

25.4 SQL En esta sección veremos una descripción general acerca de SQL, en el contexto de nuestra base de datos libros. En los ejemplos posteriores y en los ejemplos de los capítulos 26 a 28, usted podrá utilizar las consultas de SQL que describimos aquí. Las palabras clave de SQL enlistadas en la figura 25.10 se describen en las siguientes subsecciones, dentro del contexto de consultas e instrucciones de SQL completas. Las demás palabras clave de SQL se encuentran más allá del alcance de este libro. Para aprender acerca de las otras palabras clave, consulte la guía de referencia de SQL que proporciona el distribuidor del RDBMS que usted utilice. [Nota: para obtener más información acerca de SQL, consulte los recursos Web en la sección 25.15 y las lecturas recomendadas que se enlistan al final de este capítulo]. Palabra clave de SQL

Descripción

SELECT

Obtiene datos de una o más tablas.

FROM

Las tablas involucradas en la consulta. Se requiere para cada SELECT.

WERE

Los criterios de selección que determinan cuáles filas se van a recuperar, eliminar o actualizar. Es opcional en una consulta o instrucción de SQL.

GROUP BY

Criterios para agrupar filas. Es opcional en una consulta SELECT.

ORDER BY

Criterios para ordenar filas. Es opcional en una consulta SELECT.

INNER JOIN

Fusionar filas de varias tablas.

INSERT

Insertar filas en una tabla especificada.

UPDATE

Actualizar filas en una tabla especificada.

DELETE

Eliminar filas de una tabla especificada.

Figura 25.10 | Palabras clave para consultas de SQL.

25.4.1 Consulta básica SELECT Veamos ahora varias consultas de SQL que extraen información de la base de datos libros. Una consulta de SQL “selecciona” filas y columnas de una o más tablas en una base de datos. Dichas selecciones se llevan a cabo mediante consultas con la palabra clave SELECT. La forma básica de una consulta SELECT es: nombreDeTabla en la consulta anterior, el asterisco (*) indica que deben obtenerse todas las columnas de la tabla nombreDeTabla. Por ejemplo, para obtener todos los datos en la tabla autores, podemos utilizar la siguiente consulta: SELECT * FROM

SELECT * FROM autores

La mayoría de los programas no requieren todos los datos en la tabla. Para obtener sólo ciertas columnas de una tabla, reemplace el asterisco (*) con una lista separada por comas de los nombres de las columnas. Por ejemplo, para obtener solamente las columnas idAutor y apellidoPaterno para todas las filas en la tabla autores, utilice la siguiente consulta:

www.elsolucionario.net

1048 Capítulo 25

Acceso a bases de datos con JDBC

SELECT idAutor, apellidoPaterno FROM autores

Esta consulta devuelve los datos que se muestran en la figura 25.11.

idAutor

apellidoPaterno

1

Deitel

2

Deitel

3

Nieto

4

Santry

Figura 25.11 | Datos de ejemplo para idAutor y apellidoPaterno de la tabla autores.

Observación de ingeniería de software 25.2 Para la mayoría de las consultas, no debe utilizarse el asterisco (*) para especificar nombres de columnas. En general, los programadores procesan los resultados sabiendo de antemano el orden de las columnas en el resultado; por ejemplo, al seleccionar idAutor y apellidoPaterno de la tabla autores nos aseguramos que las columnas aparezcan en el resultado con idAutor como la primera columna y apellidoPaterno como la segunda. Generalmente los programas procesan las columnas de resultado especificando el número de columna en el resultado (empezando con el número 1 para la primera columna). Al seleccionar las columnas por nombre, también evitamos devolver columnas innecesarias y nos protegemos contra los cambios en el orden actual de las columnas en la(s) tabla(s).

Error común de programación 25.4 Si un programador supone que las columnas siempre se devuelven en el mismo orden de una consulta que utiliza el asterisco (*), el programa podría procesar los resultados incorrectamente. Si cambia el orden de las columnas en la(s) tabla(s), o si se agregan más columnas posteriormente, el orden de las columnas en el resultado cambia de manera acorde.

25.4.2 La cláusula WHERE En la mayoría de los casos es necesario localizar, en una base de datos, filas que cumplan con ciertos criterios de selección. Sólo se seleccionan las filas que cumplan con los criterios de selección (formalmente llamados predicados). SQL utiliza la cláusula WHERE opcional en una consulta para especificar los criterios de selección para la misma. La forma básica de una consulta con criterios de selección es: SELECT

nombreDeColumna1, nombreDeColumna2,

… FROM

nombreDeTabla

WHERE

criterios

Por ejemplo, para seleccionar las columnas titulo, numeroEdicion y copyright de la tabla titulos para las cuales la fecha de copyright sea mayor que 2005, utilice la siguiente consulta: SELECT titulo, numeroEdicion, copyright FROM titulos WHERE copyright > '2005'

En la figura 25.12 se muestra el resultado de la consulta anterior. Los criterios de la cláusula WHERE pueden contener los operadores , =, =, y LIKE. El operador LIKE se utiliza para relacionar patrones con los caracteres comodines porcentaje (%) y guión bajo (_). El relacionar patrones permite a SQL buscar cadenas que coincidan con un patrón dado. Un patrón que contenga un carácter de porcentaje (%) busca cadenas que tengan cero o más caracteres en la posición del carácter de porcentaje en el patrón. Por ejemplo, la siguiente consulta localiza las filas de todos los autores cuyo apellido paterno empiece con la letra D: SELECT idAutor, nombrePila, apellidoPaterno FROM autores WHERE apellidoPaterno LIKE 'D%'

www.elsolucionario.net

25.4

SQL 1049

La consulta anterior selecciona las dos filas que se muestran en la figura 25.13, ya que dos de los cuatro autores en nuestra base de datos tienen un apellido paterno que empieza con la letra D (seguida de cero o más caracteres). El signo de % y el operador LIKE de la cláusula WHERE indican que puede aparecer cualquier número de caracteres después de la letra D en la columna apellidoPaterno. Observe que la cadena del patrón está encerrada entre caracteres de comilla sencilla.

titulo

numeroEdicion

copyright

Visual C# How to Program

2

2006

Visual Basic 2005 How to Program

3

2006

Java How to Program

7

2007

C How to Program

5

2005

Figura 25.12 | Ejemplos de títulos con fechas de copyright posteriores a 2005 de la tabla titulos.

idAutor

nombrePila

apellidoPaterno

1

Harvey

Deitel

2

Paul

Deitel

Figura 25.13 | Autores, cuyo apellido paterno empieza con D de la tabla autores.

Tip de portabilidad 25.1 Consulte la documentación de su sistema de bases de datos para determinar si SQL es susceptible al uso de mayúsculas en su sistema, y para determinar la sintaxis de las palabras clave de SQL (por ejemplo, ¿deben estar completamente en mayúsculas, completamente en minúsculas o puede ser una combinación de ambas?).

Tip de portabilidad 25.2 Lea cuidadosamente la documentación de su sistema de bases de datos para determinar si éste soporta el operador LIKE. El SQL que describimos es soportado por la mayoría de los RDBMS, pero siempre es buena idea comprobar las características de SQL que soporta su RDBMS.

Un guión bajo ( _ ) en la cadena del patrón indica un carácter comodín individual en esa posición. Por ejemplo, la siguiente consulta localiza las filas de todos los autores cuyo apellido paterno empiece con cualquier carácter (lo que se especifica con _), seguido por la letra o, seguida por cualquier número de caracteres adicionales (lo que se especifica con %): SELECT idAutor, nombrePila, apellidoPaterno FROM autores WHERE apellidoPaterno LIKE '_o%'

La consulta anterior produce la fila que se muestra en la figura 25.14, ya que sólo un autor en nuestra base de datos tiene un apellido paterno que contiene la letra o como su segunda letra.

idAutor

nombrePila

apellidoPaterno

3

Andrew

Goldberg

Figura 25.14 | El único autor de la tabla autores cuyo apellido paterno contiene o como la segunda letra.

www.elsolucionario.net

1050 Capítulo 25

Acceso a bases de datos con JDBC

25.4.3 La cláusula ORDER BY Las filas en el resultado de una consulta pueden ordenarse en forma ascendente o descendente, mediante el uso de la cláusula ORDER BY opcional. La forma básica de una consulta con una cláusula ORDER BY es: SELECT SELECT

nombreDeColumna1, nombreDeColumna2 , … FROM nombreDeTabla ORDER BY columna ASC nombreDeColumna1, nombreDeColumna2 , … FROM nombreDeTabla ORDER BY columna DESC

en donde ASC especifica el orden ascendente (de menor a mayor), DESC especifica el orden descendente (de mayor a menor) y columna especifica la columna en la cual se basa el ordenamiento. Por ejemplo, para obtener la lista de autores en orden ascendente por apellido paterno (figura 25.15), utilice la siguiente consulta: SELECT idAutor, nombrePila, apellidoPaterno FROM autores ORDER BY apellidoPaterno ASC

Observe que el orden predeterminado es ascendente, por lo que ASC es opcional. Para obtener la misma lista de autores en orden descendente por apellido paterno (figura 25.16), utilice la siguiente consulta: SELECT idAutor, nombrePila, apellidoPaterno FROM autores ORDER BY apellidoPaterno DESC

Pueden usarse varias columnas para ordenar mediante una cláusula ORDER BY, de la siguiente forma: ORDER BY

columna1 tipoDeOrden, columna2 tipoDeOrden,

en donde tipoDeOrden puede ser columna. La consulta:

ASC

o

DESC.



Observe que el tipoDeOrden no tiene que ser idéntico para cada

SELECT idAutor, nombrePila, apellidoPaterno FROM autores ORDER BY apellidoPaterno, nombrePila

idAutor

nombrePila

apellidoPaterno

4

David

Choffnes

1

Harvey

Deitel

2

Paul

Deitel

3

Andrew

Goldberg

Figura 25.15 | Datos de ejemplo de la tabla autores ordenados en forma ascendente por la columna apellidoPaterno.

idAutor

nombrePila

apellidoPaterno

3

Andrew

Goldberg

1

Harvey

Deitel

2

Paul

Deitel

4

David

Choffnes

Figura 25.16 | Datos de ejemplo de la tabla autores ordenados en forma descendente por la columna apellidoPaterno.

www.elsolucionario.net

25.4

SQL 1051

ordena en forma ascendente todas las filas por apellido paterno, y después por nombre de pila. Si varias filas tienen el mismo valor de apellido paterno, se devuelven ordenadas por nombre de pila (figura 25.17). Las cláusulas WHERE y ORDER BY pueden combinarse en una consulta. Por ejemplo, la consulta: SELECT isbn, titulo, numeroEdicion, copyright, precio FROM titulos WHERE titulo LIKE '%How to Program' ORDER BY titulo ASC

devuelve el isbn, titulo, numeroEdicion, copyright y precio de cada libro en la tabla titulos que tenga un titulo que termine con "How to Program" y los ordena en forma ascendente, por titulo. El resultado de la consulta se muestra en la figura 25.18.

idAutor

nombrePila

apellidoPaterno

4

David

Choffner

1

Harvey

Deitel

2

Paul

Deitel

3

Andrew

Goldberg

Figura 25.17 | Datos de ejemplo de autores de la tabla autores ordenados de manera ascendente, por las columnas apellidoPaterno y nombrePila.

isbn

titulo

numeroEdicion

copyright

0132404168

C How to Program

5

2007

0131857576

C++ How to Program

5

2005

0131450913

Internet & World Wide Web How to Program

3

2004

0132222205

Java How to Program

7

2007

0131869000

Visual Basic 2005 How to Program

3

2006

013152539

Visual C# How to Program

2

2006

Figura 25.18 | Ejemplos de libros de la tabla titulos cuyos títulos terminan con How to Program, y están ordenados en forma ascendente por medio de la columna titulo.

25.4.4 Cómo fusionar datos de varias tablas: INNER JOIN Los diseñadores de bases de datos a menudo dividen los datos en tablas separadas para asegurarse de no guardar información redundante. Por ejemplo, la base de datos libros tiene las tablas autores y titulos. Utilizamos una tabla isbnAutor para almacenar los datos de la relación entre los autores y sus correspondientes títulos. Si no separáramos esta información en tablas individuales, tendríamos que incluir la información del autor con cada entrada en la tabla titulos. Esto implicaría almacenar información duplicada sobre los autores en la base de datos, para quienes hayan escrito varios libros. A menudo es necesario fusionar datos de varias tablas en un solo resultado. Este proceso, que se le conoce como unir las tablas, se especifica mediante un operador INNER JOIN en la consulta. Un operador INNER JOIN fusiona las filas de dos tablas al relacionar los valores en columnas que sean comunes para las dos tablas. La forma básica de un INNER JOIN es: SELECT nombreDeColumna1, FROM tabla1

nombreDeColumna2,



www.elsolucionario.net

1052 Capítulo 25

Acceso a bases de datos con JDBC

INNER JOIN tabla2 ON tabla1.nombreDeColumna =

tabla2.nombreDeColumna

La cláusula ON de INNER JOIN especifica las columnas de cada tabla que se comparan para determinar cuáles filas se fusionan. Por ejemplo, la siguiente consulta produce una lista de autores acompañada por los ISBNs para los libros escritos por cada autor. SELECT nombrePila, apellidoPaterno, isbn FROM autores INNER JOIN isbnAutor ON autores.idAutor = isbnAutor.idAutor ORDER BY apellidoPaterno, nombrePila

La consulta fusiona los datos de las columnas nombrePila y apellidoPaterno de la tabla autores con la columna isbn de la tabla isbnAutor, ordenando el resultado en forma ascendente por apellidoPaterno y nombrePila. Observe el uso de la sintaxis nombreDeTabla.nombreDeColumna en la cláusula ON. Esta sintaxis (a la que se le conoce como nombre calificado) especifica las columnas de cada tabla que deben compararse para unir las tablas. La sintaxis “nombreDeTabla.” es requerida si las columnas tienen el mismo nombre en ambas tablas. La misma sintaxis puede utilizarse en cualquier consulta para diferenciar entre columnas de tablas distintas que tengan el mismo nombre. En algunos sistemas pueden utilizarse nombres de tablas calificados con el nombre de la base de datos para realizar consultas entre varias bases de datos. Como siempre, la consulta puede contener una cláusula ORDER BY. En la figura 25.19 se muestra una parte de los resultados de la consulta anterior, ordenados por apellidoPaterno y nombrePila. [Nota: para ahorrar espacio dividimos el resultado de la consulta en dos columnas, cada una de las cuales contiene a las columnas nombrePila, apellidoPaterno e isbn].

Observación de ingeniería de software 25.3 Si una instrucción de SQL incluye columnas de varias tablas que tengan el mismo nombre, en la instrucción se debe anteponer a los nombres de esas columnas los nombres de sus tablas y el operador punto (por ejemplo, autores. idAutor).

Error común de programación 25.5 Si no se califican los nombres para las columnas que tengan el mismo nombre en dos o más tablas se produce un error.

nombrePila

apellidoPaterno

isbn

nombrePila

apellidoPaterno

isbn

David

Choffnes

0131828274

Paul

Deitel

0131525239

Harvey

Deitel

0131525239

Paul

Deitel

0132404168

Harvey

Deitel

0132404168

Paul

Deitel

0131869000

Harvey

Deitel

0131869000

Paul

Deitel

0132222205

Harvey

Deitel

0132222205

Paul

Deitel

0131450913

Harvey

Deitel

0131450913

Paul

Deitel

0131525239

Harvey

Deitel

0131525239

Paul

Deitel

0131857576

Harvey

Deitel

0131857576

Paul

Deitel

0131828274

Harvey

Deitel

0131828274

Andrew

Goldberg

0131450913

Figura 25.19 | Ejemplo de datos de autores e ISBNs para los libros que han escrito, en orden ascendente por apellidoPaterno y nombrePila.

www.elsolucionario.net

25.4

SQL 1053

25.4.5 La instrucción INSERT La instrucción INSERT inserta una fila en una tabla. La forma básica de esta instrucción es: INSERT INTO nombreDeTabla ( nombreDeColumna1, VALUES ( valor1, valor2, …, valorN )

nombreDeColumna2, …, nombreDeColumnaN )

en donde nombreDeTabla es la tabla en la que se va a insertar la fila. El nombreDeTabla va seguido de una lista separada por comas de nombres de columnas entre paréntesis (esta lista no es requerida si la operación INSERT especifica un valor para cada columna de la tabla en el orden correcto). La lista de nombres de columnas va seguida por la palabra clave VALUES de SQL, y una lista separada por comas de valores entre paréntesis. Los valores especificados aquí deben coincidir con las columnas especificadas después del nombre de la tabla, tanto en orden como en tipo (por ejemplo, si nombreDeColumna1 se supone que debe ser la columna nombrePila, entonces valor1 debe ser una cadena entre comillas sencillas que represente el nombre de pila). Siempre debemos enlistar explícitamente las columnas al insertar filas. Si el orden de las columnas cambia en la tabla, al utilizar solamente VALUES se puede provocar un error. La instrucción INSERT: INSERT INTO autores ( nombrePila, apellidoPaterno ) VALUES ( 'Alejandra', 'Villarreal' )

inserta una fila en la tabla autores. La instrucción indica que se proporcionan valores para las columnas nombrePila y apellidoPaterno. Los valores correspondientes son 'Alejandra' y 'Villarreal'. No especificamos un idAutor en este ejemplo, ya que idAutor es una columna autoincrementada en la tabla autores. Para cada fila que se agrega a esta tabla, MySQL asigna un valor de idAutor único que es el siguiente valor en la secuencia autoincrementada (por ejemplo: 1, 2, 3 y así sucesivamente). En este caso, Alejandra Villarreal recibiría el número 5 para idAutor. En la figura 25.20 se muestra la tabla autores después de la operación INSERT. [Nota: no todos los sistemas de administración de bases de datos soportan las columnas autoincrementadas. Consulte la documentación de su DBMS para encontrar alternativas a las columnas autoincrementadas].

idAutor

nombrePila

apellidoPaterno

1

Harvey

Deitel

2

Paul

Deitel

3

Andrew

Goldberg

4

David

Choffnes

5

Alejandra

Villarreal

Figura 25.20 | Datos de ejemplo de la tabla autores después de una operación INSERT.

Error común de programación 25.6 Por lo general, es un error especificar un valor para una columna que se autoincrementa.

Error común de programación 25.7 Las instrucciones de SQL utilizan el carácter de comilla sencilla (') como delimitador para las cadenas. Para especificar una cadena que contenga una comilla sencilla (como O’Malley) en una instrucción de SQL, la cadena debe tener dos comillas sencillas en la posición en la que aparezca el carácter de comilla sencilla en la cadena (por ejemplo, 'O''Malley'). El primero de los dos caracteres de comilla sencilla actúa como carácter de escape para el segundo. Si no se utiliza el carácter de escape en una cadena que sea parte de una instrucción de SQL, se produce un error de sintaxis de SQL.

25.4.6 La instrucción UPDATE Una instrucción UPDATE modifica los datos en una tabla. La forma básica de la instrucción UPDATE es:

www.elsolucionario.net

1054 Capítulo 25

Acceso a bases de datos con JDBC

UPDATE nombreDeTabla SET nombreDeColumna1 = valor1, nombreDeColumna2 = valor2, …, nombreDeColumnaN = valorN WHERE criterios

en donde nombreDeTabla es la tabla que se va a actualizar. El nombreDeTabla va seguido por la palabra clave SET y una lista separada por comas de los pares nombre/valor de las columnas, en el formato nombreDeColumna=valor. La cláusula WHERE opcional proporciona los criterios que determinan cuáles filas se van a actualizar. Aunque no es obligatoria, la cláusula WHERE se utiliza comúnmente, a menos que se vaya a realizar un cambio en todas las filas. La instrucción UPDATE: UPDATE autores SET apellidoPaterno = 'Garcia' WHERE apellidoPaterno = 'Villarreal' AND nombrePila = 'Alejandra'

actualiza una fila en la tabla autores. La instrucción indica que apellidoPaterno recibirá el valor Garcia para la fila en la que apellidoPaterno sea igual a Villarreal y nombrePila sea igual a Alejandra. [Nota: si hay varias filas con el mismo nombre de pila “Alejandra” y el apellido paterno “Villarreal”, esta instrucción modificará a todas esas filas para que tengan el apellido paterno “Garcia”]. Si conocemos el idAutor desde antes de realizar la operación UPDATE (tal vez porque lo hayamos buscado con anterioridad), la cláusula WHERE se puede simplificar de la siguiente manera: WHERE idAutor = 5

En la figura 25.21 se muestra la tabla autores después de realizar la operación UPDATE.

idAutor

nombrePila

apellidoPaterno

1

Harvey

Deitel

2

Paul

Deitel

3

Andrew

Goldberg

4

David

Choffnes

5

Alejandra

Garcia

Figura 25.21 | Datos de ejemplo de la tabla autores después de una operación UPDATE.

25.4.7 La instrucción DELETE Una instrucción DELETE de SQL elimina filas de una tabla. La forma básica de una instrucción DELETE es: DELETE FROM

nombreDeTabla

WHERE

criterios

en donde nombreDeTabla es la tabla de la que se van a eliminar filas. La cláusula WHERE opcional especifica los criterios utilizados para determinar cuáles filas eliminar. Si se omite esta cláusula, se eliminan todas las filas de la tabla. La instrucción DELETE: DELETE FROM autores WHERE apellidoPaterno = 'Garcia' AND nombrePila = 'Alejandra'

elimina la fila de Alejandra Garcia en la tabla autores. Si conocemos el idAutor desde antes de realizar la operación DELETE, la cláusula WHERE puede simplificarse de la siguiente manera: WHERE idAutor = 5

En la figura 25.22 se muestra la tabla autores después de realizar la operación DELETE.

www.elsolucionario.net

25.5

Instrucciones para instalar MySQL y MySQL Connector/J 1055

idAutor

nombrePila

apellidoPaterno

1

Harvey

Deitel

2

Paul

Deitel

3

Andrew

Goldberg

4

David

Choffnes

Figura 25.22 | Datos de ejemplo de la tabla autores después de una operación DELETE.

25.5 Instrucciones para instalar MySQL y MySQL Connector/J MySQL 5.0 Community Edition es un sistema de administración de bases de datos de código fuente abierto que se ejecuta en muchas plataformas, incluyendo Windows, Solaris, Linux y Macintosh. En el sitio www.mysql.com encontrará toda la información acerca de MySQL. Los ejemplos en las secciones 25.8 y 25.9 manipulan bases de datos de MySQL.

Instalación de MySQL Para instalar MySQL Community Edition: 1. Para aprender acerca de los requerimientos de instalación para su plataforma, visite el sitio dev.mysql. com/doc/refman/5.0/en/general-installation-issues.html (para ver esta información en español, visite el sitio dev.mysql.com/doc/refman/5.0/es/general-installation-issues.html). 2. Visite dev.mysql.com/downloads/mysql/5.0.html y descargue el instalador para su plataforma. Para los ejemplos de MySQL en este capítulo, sólo necesita el paquete Windows Essentials en Microsoft Windows, o el paquete Standard en la mayoría de las otras plataformas. [Nota: para estas instrucciones, vamos a suponer que está utilizando Microsoft Windows. En el sitio dev.mysql.com/doc/ refman/5.0/en/installing.html (o dev.mysql.com/doc/refman/5.0/es/installing.html en español) encontrará las instrucciones completas de instalación para las otras plataformas]. 3. Haga doble clic en el archivo mysql-essential-5.0.27-win32.msi para iniciar el instalador. [Nota: el nombre de este archivo puede diferir, dependiendo de la versión actual de MySQL 5.0]. 4. Seleccione la opción Typical (Típica) en Setup Type (Tipo de instalación) y haga clic en Next > (Siguiente). Después haga clic en Install (Instalar). Cuando termine la instalación, el programa le pedirá que configure una cuenta en MySQL.com. Si no desea hacerlo, seleccione Skip Sign-up (Omitir registro) y haga clic en Next > (Siguiente). Después de completar el proceso de registro o de omitirlo, puede configurar el servidor de MySQL. Haga clic en Finish (Terminar) para iniciar el MySQL Server Instance Configuration Wizard (Asistente para la configuración de una instancia de MySQL Server). Para configurar el servidor: 1. Haga clic en Next > (Siguiente); después seleccione Standard Configuration (Configuración estándar) y haga clic en Next > otra vez. 2. Tiene la opción de instalar MySQL como servicio Windows, lo cual permite al servidor de MySQL empezar a ejecutarse automáticamente cada vez que su sistema se inicie. Para nuestros ejemplos esto no es necesario, por lo que puede desactivar la opción Install as a Windows Service (Instalar como servicio Windows), y después haga clic en la opción Include Bin Directory in Windows PATH (Incluir directorio Bin en la ruta PATH de Windows). Esto le permitirá usar los comandos de MySQL en el Símbolo del sistema de Windows. 3. Haga clic en Next > y después en Execute (Ejecutar) para llevar a cabo la configuración del servidor. 4. Haga clic en Finish (Terminar) para cerrar el asistente.

www.elsolucionario.net

1056 Capítulo 25

Acceso a bases de datos con JDBC

Instalación de MySQL Connector/J Para usar MySQL con JDBC, también necesita instalar MySQL Connector/J (la J representa a Java): un controlador de JDBC que permite a los programas usar JDBC para interactuar con MySQL. Puede descargar MySQL Connector/J de dev.mysql.com/downloads/connector/j/5.0.html

La documentación para Connector/J se encuentra en dev.mysql.com/doc/connector/j/en/connector-j. html. Al momento de escribir este libro, la versión actual disponible en forma general de MySQL Connector/J es 5.0.4. Para instalar MySQL Connector/J: 1. Descargue el archivo mysql-connector-java-5.0.4.zip. 2. Abra mysql-connector-java-5.0.4.zip con un extractor de archivos, como WinZip (www.winzip. com). Extraiga su contenido en la unidad C:\. Esto creará un directorio llamado mysql-connectorjava-5.0.4. La documentación para MySQL Connector/J está en el archivo connector-j.pdf en el subdirectorio docs de mysql-connector-java-5.0.4, o puede verla en línea, en el sitio dev.mysql. com/doc/connector/j/en/connector-j.html.

25.6 Instrucciones para establecer una cuenta de usuario de MySQL Para que los ejemplos de MySQL se ejecuten correctamente, necesita configurar una cuenta de usuario que permita a los usuarios crear, eliminar y modificar una base de datos. Una vez instalado MySQL, siga los pasos que se muestran a continuación para configurar una cuenta de usuario (en estos pasos asumimos que MySQL está instalado en su directorio predeterminado): 1. Abra una ventana de Símbolo del sistema e inicie el servidor de bases de datos, ejecutando el comando mysqld-nt.exe. Observe que este comando no tiene salida; simplemente inicia el servidor MySQL. No cierre esta ventana, de lo contrario el servidor dejará de ejecutarse. 2. A continuación, inicie el monitor de MySQL para que pueda configurar una cuenta de usuario; abra otra ventana de Símbolo del sistema y ejecute el comando mysql –h localhost –u root

La opción –h indica el host (computadora) en el que se está ejecutando el servidor MySQL; en este caso, es su equipo local (localhost). La opción –u indica la cuenta de usuario que se utilizará para iniciar sesión en el servidor; root es la cuenta de usuario predeterminada que se crea durante la instalación, para que usted pueda configurar el servidor. Una vez que inicie sesión, aparecerá un indicador mysql> en el que podrá escribir comandos para interactuar con el servidor MySQL. 3. En el indicador mysql>, escriba USE mysql;

para seleccionar la base de datos incrustada, llamada mysql, la cual almacena información relacionada con el servidor, como las cuentas de usuario y sus privilegios para interactuar con el servidor. Observe que cada comando debe terminar con punto y coma. Para confirmar el comando, MySQL genera el mensaje “Database changed.” (La base de datos cambió). 4. A continuación, agregue la cuenta de usuario jhtp7 a la base de datos incrustada mysql. Esta base de datos contiene una tabla llamada user, con columnas que representan el nombre del usuario, su contraseña y varios privilegios. Para crear la cuenta de usuario jhtp7 con la contraseña jhtp7, ejecute los siguientes comandos desde el indicador mysql>: create user 'jhtp7'@'localhost' identified by 'jhtp7'; grant select, insert, update, delete, create, drop, references, execute on *.* to 'jhtp7’@’localhost';

www.elsolucionario.net

25.8

Manipulación de bases de datos con JDBC 1057

Esto crea el usuario jhtp7 con los privilegios necesarios para crear las bases de datos utilizadas en este capítulo, y para manipular esas bases de datos. Por último, 5. Escriba el comando exit;

para terminar el monitor MySQL.

25.7 Creación de la base de datos libros en MySQL

Para cada una de las bases de datos MySQL que veremos en este libro, proporcionamos una secuencia de comandos SQL en un archivo con la extensión .sql que configura la base de datos y sus tablas. Puede ejecutar estas secuencias de comandos en el monitor de MySQL. En el directorio de ejemplos de este capítulo, encontrará la secuencia de comandos SQL libros.sql para crear la base de datos libros. Para los siguientes pasos, vamos a suponer que el servidor MySQL (mysqld-nt.exe) sigue ejecutándose. Para ejecutar la secuencia de comandos libros.sql: 1. Abra una ventana de Símbolo del sistema y utilice el comando cd para cambiar al directorio en el que se encuentra la secuencia de comandos libros.sql. 2. Inicie el monitor de MySQL, escribiendo mysql –h localhost –u jhtp7 –p

La opción –p hará que el monitor le pida la contraseña para el usuario escriba la contraseña jhtp7.

jhtp7.

Cuando ocurra esto,

3. Ejecute la secuencia de comandos, escribiendo source libros.sql;

Esto creará un nuevo directorio llamado libros en el directorio encuentra en C:\Archivos de programa\MySQL\MySQL Server da). Este nuevo directorio contiene la base de datos libros.

data del servidor (en Windows, se 5.0\data de manera predetermina-

4. Escriba el comando exit;

para terminar el monitor de MySQL. Ahora está listo para continuar con el primer ejemplo de JDBC.

25.8 Manipulación de bases de datos con JDBC En esta sección presentaremos dos ejemplos. El primero le enseñará cómo conectarse a una base de datos y hacer consultas en ella. El segundo ejemplo le demostrará cómo mostrar en pantalla el resultado de la consulta.

25.8.1 Cómo conectarse y realizar consultas en una base de datos El ejemplo de la figura 25.23 realiza una consulta simple en la base de datos libros para obtener toda la tabla autores y mostrar los datos. El programa muestra cómo conectarse a la base de datos, hacer una consulta en la misma y procesar el resultado. En la siguiente discusión presentaremos los aspectos clave del programa relacionados con JDBC. [Nota: en las secciones 25.5 a 25.7 se muestra cómo iniciar el servidor MySQL, configurar una cuenta de usuario y crear la base de datos libros. Estos pasos deben realizarse antes de ejecutar el programa de la figura 25.23].

1 2 3 4

// Fig. 25.23: MostrarAutores.java // Muestra el contenido de la tabla autores. import java.sql.Connection; import java.sql.Statement;

Figura 25.23 | Cómo mostrar el contenido de la tabla autores. (Parte 1 de 3).

www.elsolucionario.net

1058 Capítulo 25

5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

import import import import

Acceso a bases de datos con JDBC

java.sql.DriverManager; java.sql.ResultSet; java.sql.ResultSetMetaData; java.sql.SQLException;

public class MostrarAutores { // nombre del controlador de JDBC y URL de la base de datos static final String CONTROLADOR = "com.mysql.jdbc.Driver"; static final String URL_BASEDATOS = "jdbc:mysql://localhost/libros"; // inicia la aplicación public static void main( String args[] ) { Connection conexion = null; // maneja la conexión Statement instruccion = null; // instrucción de consulta ResultSet conjuntoResultados = null; // maneja los resultados // se conecta a la base de datos libros y realiza una consulta try { // carga la clase controlador Class.forName( CONTROLADOR ); // establece la conexión a la base de datos conexion = DriverManager.getConnection( URL_BASEDATOS, "jhtp7", "jhtp7" ); // crea objeto Statement para consultar la base de datos instruccion = conexion.createStatement(); // consulta la base de datos conjuntoResultados = instruccion.executeQuery( "SELECT IDAutor, nombrePila, apellidoPaterno FROM autores" ); // procesa los resultados de la consulta ResultSetMetaData metaDatos = conjuntoResultados.getMetaData(); int numeroDeColumnas = metaDatos.getColumnCount(); System.out.println( "Tabla Autores de la base de datos Libros:\n" ); for ( int i = 1; i ( modeloTabla ); tablaResultados.setRowSorter( sorter ); setSize( 500, 250 ); // establece el tamaño de la ventana setVisible( true ); // muestra la ventana // crea componente de escucha para botonFiltro botonFiltro.addActionListener( new ActionListener() { // pasa el texto del filtro al componente de escucha public void actionPerformed( ActionEvent e ) { String texto = textoFiltro.getText(); if ( texto.length() == 0 ) sorter.setRowFilter( null ); else { try { sorter.setRowFilter( RowFilter.regexFilter( texto ) ); } // fin de try catch ( PatternSyntaxException pse )

Figura 25.28 | Visualización del contenido de la base de datos libros. (Parte 3 de 5).

www.elsolucionario.net

25.8

152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205

Manipulación de bases de datos con JDBC 1071

{ JOptionPane.showMessageDialog( null, "Patron de exp reg incorrecto", "Patron de exp reg incorrecto", JOptionPane.ERROR_MESSAGE ); } // fin de catch } // fin de else } // fin del método actionPerfomed } // fin de la clase interna anónima ); // fin de la llamada a addActionLister } // fin de try catch ( ClassNotFoundException noEncontroClase ) { JOptionPane.showMessageDialog( null, "No se encontro controlador de base de datos", "No se encontro el controlador", JOptionPane.ERROR_MESSAGE ); System.exit( 1 ); // termina la aplicación } // fin de catch catch ( SQLException excepcionSql ) { JOptionPane.showMessageDialog( null, excepcionSql.getMessage(), "Error en base de datos", JOptionPane.ERROR_MESSAGE ); // verifica que esté cerrada la conexión a la base de datos modeloTabla.desconectarDeBaseDatos(); System.exit( 1 ); // termina la aplicación } // fin de catch // cierra la ventana cuando el usuario sale de la aplicación (se sobrescribe // el valor predeterminado de HIDE_ON_CLOSE) setDefaultCloseOperation( DISPOSE_ON_CLOSE ); // verifica que esté cerrada la conexión a la base de datos cuando el usuario sale de la aplicación addWindowListener( new WindowAdapter() { // se desconecta de la base de datos y sale cuando se ha cerrado la ventana public void windowClosed( WindowEvent evento ) { modeloTabla.desconectarDeBaseDatos(); System.exit( 0 ); } // fin del método windowClosed } // fin de la clase interna WindowAdapter ); // fin de la llamada a addWindowListener } // fin del constructor de MostrarResultadosConsulta // ejecuta la aplicación public static void main( String args[] ) { new MostrarResultadosConsulta(); } // fin de main } // fin de la clase MostrarResultadosConsulta

Figura 25.28 | Visualización del contenido de la base de datos libros. (Parte 4 de 5).

www.elsolucionario.net

1072 Capítulo 25

Acceso a bases de datos con JDBC

Figura 25.28 | Visualización del contenido de la base de datos libros. (Parte 5 de 5). En las líneas 86 a 125 se registra un manejador de eventos para el botonEnviar en el que el usuario hace clic para enviar una consulta a la base de datos. Cuando el usuario hace clic en el botón, el método actionPerformed (líneas 91 a 123) invoca al método establecerConsulta de la clase ResultSetTableModel para ejecutar la nueva consulta. Si la consulta del usuario falla (por ejemplo, debido a un error de sintaxis en la entrada del usuario), en las líneas 108 y 109 se ejecuta la consulta predeterminada. Si la consulta predeterminada también falla, podría haber un error más grave, por lo que en la línea 118 se asegura que se cierre la conexión a la base de datos y en la línea 120 se sale del programa. Las capturas de pantalla de la figura 25.28 muestran los resultados de dos consultas. La primera captura de pantalla muestra la consulta predeterminada, en la que se recuperan todos los datos de la tabla autores de la base de datos libros. La segunda captura de pantalla muestra una consulta que selecciona el nombre de pila y el apellido paterno de cada autor en la tabla autores, y combina esa información con el título y número de edición de la tabla titulos. Pruebe a introducir sus propias consultas en el área de texto y haga clic en el botón Enviar consulta para enviar la consulta. A partir de Java SE 6, los objetos JTable ahora permiten a los usuarios ordenar filas en base a los datos en una columna específica. En las líneas 127 y 128 se utiliza la clase TableRowSorter (del paquete javax.swing. table) para crear un objeto que utilice nuestro objeto ResultSetTableModel para ordenar filas en el objeto JTable que muestre los resultados de la consulta. Cuando el usuario hace clic en el título de una columna

www.elsolucionario.net

25.9

La interfaz RowSet 1073

específica del objeto JTable, el objeto TableRowSorter interactúa con el objeto TableModel subyacente para reordenar las filas, con base en los datos en esa columna. En la línea 129 se utiliza el método setRowSorter del método JTable para especificar el objeto TableRowSorter para tablaResultados. Otra de las nuevas características de los objetos JTable es la habilidad de ver subconjuntos de los datos del objeto TableModel subyacente. A esto se le conoce como filtrar los datos. En las líneas 134 a 160 se registra un manejador de eventos para el botonFiltro que el usuario oprime para filtrar los datos. En el método actionPerformed (líneas 138 a 158), en la línea 140 se obtiene el texto de filtro. Si el usuario no especificó un texto de filtro, en la línea 143 se utiliza el método setRowFilter de JTable para eliminar cualquier filtro anterior, estableciendo el filtro en null. En caso contrario, en las líneas 148 y 149 se utiliza setRowFilter para especificar un objeto RowFilter (del paquete javax.swing) con base en la entrada del usuario. La clase RowFilter cuenta con varios métodos para crear filtros. El método static regexFilter recibe un objeto String que contiene un patrón de expresión regular como argumento, y un conjunto opcional de índices que especifican cuáles columnas se van a filtrar. Si no se especifican índices, entonces se busca en todas las columnas. En este ejemplo, el patrón de expresión regular es el texto que escribió el usuario. Una vez establecido el filtro, se actualizan los datos mostrados en el objeto JTable con base en el objeto TableModel filtrado.

25.9 La interfaz RowSet

En los ejemplos anteriores, aprendido a realizar consultas en una base de datos al establecer en forma explícita una conexión (Connection) a la base de datos, preparar una instrucción (Statement) para consultar la base de datos y ejecutar la consulta. En esta sección demostraremos la interfaz RowSet, la cual configura la conexión a la base de datos y prepara instrucciones de consulta en forma automática. La interfaz RowSet proporciona varios métodos establecer (get) que nos permiten especificar las propiedades necesarias para establecer una conexión (como el URL de la base de datos, el nombre de usuario y la contraseña) y crear un objeto Statement (como una consulta). RowSet también cuenta con varios métodos obtener (get) para devolver estas propiedades. Hay dos tipos de objetos RowSet: conectados y desconectados. Un objeto RowSet conectado se conecta a la base de datos una sola vez, y permanece conectado hasta que termina la aplicación. Un objeto RowSet desconectado se conecta a la base de datos, ejecuta una consulta para obtener los datos de la base de datos y después cierra la conexión. Un programa puede cambiar los datos en un objeto RowSet desconectado, mientras éste se encuentre desconectado. Los datos modificados pueden actualizarse en la base de datos, después de que un objeto RowSet desconectado reestablece la conexión a la base de datos. El paquete javax.sql.rowset contiene dos subinterfaces de RowSet: JdbcRowSet y CachedRowSet. JdbcRowSet, un objeto RowSet conectado, actúa como una envoltura alrededor de un objeto ResultSet y nos permite desplazarnos a través de las filas en el objeto ResultSet, y también actualizarlas. Recuerde que, de manera predeterminada, un objeto ResultSet no es desplazable y es de sólo lectura; debemos establecer explícitamente la constante de tipo del conjunto de resultados a TYPE_SCROLL_INSENSITIVE y establecer la constante de concurrencia del conjunto de resultados CONCUR_UPDATABLE para hacer que un objeto ResultSet sea desplazable y pueda actualizarse. Un objeto JdbcRowSet es desplazable y puede actualizarse de manera predeterminada. CachedRowSet, un objeto RowSet desconectado, coloca los datos de un objeto ResultSet en caché de memoria y los desconecta de la base de datos. Al igual que un objeto JdbcRowSet, un objeto CachedRowSet es desplazable y puede actualizarse de manera predeterminada. Un objeto CachedRowSet también es serializable, por lo que puede pasarse de una aplicación de Java a otra mediante una red, como Internet. Sin embargo, CachedRowSet tiene una limitación: la cantidad de datos que pueden almacenarse en memoria es limitada. El paquete javax. sql.rowset contiene otras tres subinterfaces de RowSet. Para obtener detalles de estas interfaces, visite java. sun.com/javase/6/docs/technotes/guides/jdbc/getstart/rowsetImpl.html.

Tip de portabilidad 25.5 Un objeto RowSet puede proporcionar capacidad de desplazamiento a los controladores que no tienen soporte para objetos ResultSet desplazables.

En la figura 25.29 se reimplementa el ejemplo de la figura 25.23, usando un objeto RowSet. En vez de establecer la conexión y crear un objeto Statement de manera explícita, en la figura 25.29 utilizamos un objeto JdbcRowSet para crear los objetos Connection y Statement de manera automática.

www.elsolucionario.net

1074 Capítulo 25

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

Acceso a bases de datos con JDBC

// Fig. 25.29: PruebaJdbcRowSet.java // Visualización del contenido de la tabla autores, mediante el uso de JdbcRowSet. import java.sql.ResultSetMetaData; import java.sql.SQLException; import javax.sql.rowset.JdbcRowSet; import com.sun.rowset.JdbcRowSetImpl; // implementación de JdbcRowSet de Sun public class PruebaJdbcRowSet { // nombre del controlador de JDBC y URL de la base de datos static final String CONTROLADOR = "com.mysql.jdbc.Driver"; static final String URL_BASEDATOS = "jdbc:mysql://localhost/libros"; static final String NOMBREUSUARIO = "jhtp7"; static final String CONTRASENIA = "jhtp7"; // el constructor se conecta a la base de datos, la consulta, procesa // los resultados y los muestra en la ventana public PruebaJdbcRowSet() { // se conecta a la base de datos libros y la consulta try { Class.forName( CONTROLADOR ); // especifica las propiedades del objeto JdbcRowSet JdbcRowSet rowSet = new JdbcRowSetImpl(); rowSet.setUrl( URL_BASEDATOS ); // establece URL de la base de datos rowSet.setUsername( NOMBREUSUARIO ); // establece el nombre de usuario rowSet.setPassword( CONTRASENIA ); // establece contraseña rowSet.setCommand( "SELECT * FROM autores" ); // establece la consulta rowSet.execute(); // ejecuta la consulta // procesa los resultados de la consulta ResultSetMetaData metaDatos = rowSet.getMetaData(); int numeroDeColumnas = metaDatos.getColumnCount(); System.out.println( "Tabla Autores de la base de datos Libros:\n" ); // muestra el encabezado del objeto RowSet for ( int i = 1; i , escriba connect 'jdbc:derby:LibretaDirecciones;create=true;user=jhtp7;password=jhtp7';

para crear la base de datos LibretaDirecciones en el directorio actual. Este comando también crea el usuario jhtp7 con la contraseña jhtp7 para acceder a la base de datos. 7. Para crear la tabla de la base de datos e insertar los datos de ejemplo, escriba run 'direccion.sql';

8. Para terminar la herramienta de línea de comandos de Java DB, escriba exit;

Ahora está listo para ejecutar la aplicación LibretaDirecciones en la sección 25.12.

25.11 Objetos PreparedStatement

La interfaz PreparedStatement nos permite crear instrucciones SQL compiladas, que se ejecutan con más eficiencia que los objetos Statement. Las instrucciones PreparedStatement también pueden especificar parámetros, lo cual las hace más flexibles que las instrucciones Statement. Los programas pueden ejecutar la misma

www.elsolucionario.net

25.11

Objetos PreparedStatement 1077

consulta varias veces, con distintos valores para los parámetros. Por ejemplo, en la base de datos libros, tal vez sea conveniente localizar todos los libros para un autor con un apellido paterno y primer nombre específicos, y ejecutar esa consulta para varios autores. Con un objeto PreparedStatement, esa consulta se define de la siguiente manera: PreparedStatement librosAutor = connection.prepareStatement( "SELECT apellidoPaterno, primerNombre, titulo " + "FROM autores INNER JOIN isbnAutor " + "ON autores.idAutor=isbnAutor.idAutor " + "INNER JOIN titulos " + "ON isbnAutor.isbn=titulos.isbn " + "WHERE apellidoPaterno = ? AND primerNombre = ?" );

Los dos signos de interrogación (?) en la última línea de la instrucción SQL anterior son receptáculos para valores que se pasarán como parte de la consulta en la base de datos. Antes de ejecutar una instrucción PreparedStatement, el programa debe especificar los valores de los parámetros mediante el uso de los métodos establecer (set) de la interfaz PreparedStatement. Para la consulta anterior, ambos parámetros son cadenas que pueden establecerse con el método setString de PreparedStatement, como se muestra a continuación: librosAutor.setString( 1, "Deitel" ); librosAutor.setString( 2, "Paul" );

El primer argumento del método setString representa el número del parámetro que se va a establecer, y el segundo argumento es el valor de ese parámetro. Los números de los parámetros se cuentan a partir de 1, empezando con el primer signo de interrogación (?). Cuando el programa ejecuta la instrucción PreparedStatement anterior con los valores de los parámetros que se muestran aquí, la instrucción SQL que se pasa a la base de datos es SELECT apellidoPaterno, primerNombre, titulo FROM autores INNER JOIN isbnAutor ON autores.idAutor=isbnAutor.idAutor INNER JOIN titulos ON isbnAutor.isbn=titulos.isbn WHERE apellidoPaterno = 'Deitel' AND primerNombre = 'Paul'

El método setString escapa de manera automática los valores de los parámetros String según sea necesario. Por ejemplo, si el apellido paterno es O’Brien, la instrucción librosAutor.setString( 1, "O'Brien" );

escapa el carácter ' en O’Brien, sustituyéndolo con dos caracteres de comilla sencilla.

Tip de rendimiento 25.2 Las instrucciones PreparedStatement son más eficientes que las instrucciones Statement al ejecutar instrucciones SQL varias veces, y con distintos valores para los parámetros.

Tip para prevenir errores 25.1 Use las instrucciones PreparedStatement con parámetros para las consultas que reciban valores String como argumentos, para asegurar que los objetos String utilicen comillas de manera apropiada en la instrucción SQL.

La interfaz PreparedStatement proporciona métodos establecer (set) para cada tipo de SQL soportado. Es importante utilizar el método establecer que sea apropiado para el tipo de SQL del parámetro en la base de datos; las excepciones SQLException ocurren cuando un programa trata de convertir el valor de un parámetro en un tipo incorrecto. Para una lista completa de los métodos establecer (set) de la interfaz PreparedStatement, consulte la página Web java.sun.com/javase/6/docs/api/java/sql/PreparedStatement.html.

www.elsolucionario.net

1078 Capítulo 25

Acceso a bases de datos con JDBC

Aplicación “libreta de direcciones” que utiliza instrucciones PreparedStatement Ahora presentaremos una aplicación “libreta de direcciones” que nos permite explorar las entradas existentes, agregar nuevas entradas y buscar entradas con un apellido paterno específico. Nuestra base de datos LibretaDirecciones de JavaDB contiene una tabla Direcciones con las columnas idDireccion, primerNombre, apellidoPaterno, email y numeroTelefonico. La columna idDireccion se denomina columna de identidad. Ésta es la manera estándar de SQL para representar una columna autoincrementada. La secuencia de comandos SQL que proporcionamos para esta base de datos utiliza la palabra clave IDENTITY de SQL para marcar la columna idDireccion como una columna de identidad. Para obtener más información acerca de la palabra IDENTITY y crear bases de datos, consulte la Guía para el desarrollador de Java DB en developers.sun.com/prodtech/javadb/ reference/docs/10.2.1.6/devguide/index.html. Nuestra aplicación de libro de direcciones consiste en tres clases: Persona (figura 25.30), ConsultasPersona (figura 25.31) y MostrarLibretaDirecciones (figura 25.32). La clase Persona es una clase simple que representa a una persona en la libreta de direcciones. Esta clase contiene campos para el ID de dirección, primer nombre, apellido paterno, dirección de e-mail y número telefónico, así como métodos establecer y obtener para manipular esos campos.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

// Fig. 25.30: Persona.java // La clase Persona representa una entrada en una libreta de direcciones. public class Persona { private int idDireccion; private String primerNombre; private String apellidoPaterno; private String email; private String numeroTelefonico; // constructor sin argumentos public Persona() { } // fin del constructor de Persona sin argumentos // constructor public Persona( int id, String nombre, String apellido, String direccionEmail, String telefono ) { establecerIDDireccion( id ); establecerPrimerNombre( nombre ); establecerApellidoPaterno( apellido ); establecerEmail( direccionEmail ); establecerNumeroTelefonico( telefono ); } // fin del constructor de Persona con cinco argumentos // establece el objeto idDireccion public void establecerIDDireccion( int id ) { idDireccion = id; } // fin del método establecerIDDireccion // devuelve el valor de idDireccion public int obtenerIDDireccion() { return idDireccion; } // fin del método obtenerIDDireccion // establece el primerNombre public void establecerPrimerNombre( String nombre )

Figura 25.30 | La clase Persona representa una entrada en un objeto LibretaDirecciones. (Parte 1 de 2).

www.elsolucionario.net

25.11

41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86

Objetos PreparedStatement 1079

{ primerNombre = nombre; } // fin del método establecerPrimerNombre // devuelve el primer nombre public String obtenerPrimerNombre() { return primerNombre; } // fin del método obtenerPrimerNombre // establece el apellidoPaterno public void establecerApellidoPaterno( String apellido ) { apellidoPaterno = apellido; } // fin del método establecerApellidoPaterno // devuelve el apellido paterno public String obtenerApellidoPaterno() { return apellidoPaterno; } // fin del método obtenerApellidoPaterno // establece la dirección de email public void establecerEmail( String direccionEmail ) { email = direccionEmail; } // fin del método establecerEmail // devuelve la dirección de email public String obtenerEmail() { return email; } // fin del método obtenerEmail // establece el número telefónico public void establecerNumeroTelefonico( String telefono ) { numeroTelefonico = telefono; } // fin del método establecerNumeroTelefonico // devuelve el número telefónico public String obtenerNumeroTelefonico() { return numeroTelefonico; } // fin del método obtenerNumeroTelefonico } // fin de la clase Persona

Figura 25.30 | La clase Persona representa una entrada en un objeto LibretaDirecciones. (Parte 2 de 2).

La clase ConsultasPersona La clase ConsultasPersona (figura 25.31) maneja la conexión a la base de datos de la aplicación libreta de direcciones y crea las instrucciones PreparedStatement que utiliza la aplicación para interactuar con la base de datos. En las líneas 18 a 20 se declaran tres variables PreparedStatement. El constructor (líneas 23 a 49) se conecta con la base de datos en las líneas 27 y 28. Observe que no utilizamos Class.forName para cargar el controlador de la base de datos para Java DB, como hicimos en los ejemplos que utilizan MySQL en secciones anteriores de este capítulo. JDBC 4.0, que forma parte de Java SE 6, soporta el descubrimiento automático de controladores; ya no tenemos que cargar el controlador de la base de datos por adelantado. Al momento de escribir este libro, esta característica se encuentra en proceso de implementarse en MySQL.

www.elsolucionario.net

1080 Capítulo 25

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57

Acceso a bases de datos con JDBC

// Fig. 25.31: ConsultasPersona.java // Instrucciones PreparedStatement utilizadas por la aplicación Libreta de direcciones import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; import java.util.ArrayList; public class ConsultasPersona { private static final String URL = "jdbc:derby:LibretaDirecciones"; private static final String NOMBREUSUARIO = "jhtp7"; private static final String CONTRASENIA = "jhtp7"; private private private private

Connection conexion = null; // maneja la conexión PreparedStatement seleccionarTodasLasPersonas = null; PreparedStatement seleccionarPersonasPorApellido = null; PreparedStatement insertarNuevaPersona = null;

// constructor public ConsultasPersona() { try { conexion = DriverManager.getConnection( URL, NOMBREUSUARIO, CONTRASENIA ); // crea una consulta que selecciona todas las entradas en la LibretaDirecciones seleccionarTodasLasPersonas = conexion.prepareStatement( "SELECT * FROM Direcciones" ); // crea una consulta que selecciona las entradas con un apellido específico seleccionarPersonasPorApellido = conexion.prepareStatement( "SELECT * FROM Direcciones WHERE ApellidoPaterno = ?" ); // crea instrucción insert para agregar una nueva entrada en la base de 39 datos insertarNuevaPersona = conexion.prepareStatement( "INSERT INTO Direcciones " + "( PrimerNombre, ApellidoPaterno, Email, NumeroTelefonico ) " + "VALUES ( ?, ?, ?, ? )" ); } // fin de try catch ( SQLException excepcionSql ) { excepcionSql.printStackTrace(); System.exit( 1 ); } // fin de catch } // fin del constructor de ConsultasPersona // selecciona todas las direcciones en la base de datos public List< Persona > obtenerTodasLasPersonas() { List< Persona > resultados = null; ResultSet conjuntoResultados = null; try

Figura 25.31 | Una interfaz que almacena todas las consultas para que las utilice un objeto LibretaDirecciones. (Parte 1 de 4).

www.elsolucionario.net

25.11

58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102

Objetos PreparedStatement 1081

{ // executeQuery devuelve ResultSet que contiene las entradas que coinciden conjuntoResultados = seleccionarTodasLasPersonas.executeQuery(); resultados = new ArrayList< Persona >(); while ( conjuntoResultados.next() ) { resultados.add( new Persona( conjuntoResultados.getInt( "idDireccion" ), conjuntoResultados.getString( "primerNombre" ), conjuntoResultados.getString( "apellidoPaterno" ), conjuntoResultados.getString( "email" ), conjuntoResultados.getString( "numeroTelefonico" ) ) ); } // fin de while } // fin de try catch ( SQLException excepcionSql ) { excepcionSql.printStackTrace(); } // fin de catch finally { try { conjuntoResultados.close(); } // fin de try catch ( SQLException excepcionSql ) { excepcionSql.printStackTrace(); close(); } // fin de catch } // fin de finally return resultados; } // fin del método obtenerTodasLasPersonaas // selecciona persona por apellido paterno public List< Persona > obtenerPersonasPorApellido( String nombre ) { List< Persona > resultados = null; ResultSet conjuntoResultados = null;

103 104 105 106 107 108 109 110 111 112 113 114

try { seleccionarPersonasPorApellido.setString( 1, nombre ); // especifica el apellido paterno // executeQuery devuelve ResultSet que contiene las entradas que coinciden conjuntoResultados = seleccionarPersonasPorApellido.executeQuery(); resultados = new ArrayList< Persona >(); while ( conjuntoResultados.next() ) { resultados.add( new Persona( conjuntoResultados.getInt( "idDireccion" ), conjuntoResultados.getString( “primerNombre" ), conjuntoResultados.getString( "apellidoPaterno" ),

Figura 25.31 | Una interfaz que almacena todas las consultas para que las utilice un objeto LibretaDirecciones. (Parte 2 de 4).

www.elsolucionario.net

1082 Capítulo 25

115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172

Acceso a bases de datos con JDBC

conjuntoResultados.getString( "email" ), conjuntoResultados.getString( "numeroTelefonico" ) ) ); } // fin de while } // fin de try catch ( SQLException excepcionSql ) { excepcionSql.printStackTrace(); } // fin de catch finally { try { conjuntoResultados.close(); } // fin de try catch ( SQLException excepcionSql ) { excepcionSql.printStackTrace(); close(); } // fin de catch } // fin de finally return resultados; } // fin del método obtenerPersonasPorApellido // agrega una entrada public int agregarPersona( String pnombre, String apaterno, String email, String num ) { int resultado = 0; // establece los parámetros, después ejecuta insertarNuevaPersona try { insertarNuevaPersona.setString( 1, pnombre ); insertarNuevaPersona.setString( 2, apaterno ); insertarNuevaPersona.setString( 3, email ); insertarNuevaPersona.setString( 4, num ); // inserta la nueva entrada; devuelve # de filas actualizadas resultado = insertarNuevaPersona.executeUpdate(); } // fin de try catch ( SQLException excepcionSql ) { excepcionSql.printStackTrace(); close(); } // fin de catch return resultado; } // fin del método agregarPersona // cierra la conexión a la base de datos public void close() { try { conexion.close(); } // fin de try catch ( SQLException excepcionSql )

Figura 25.31 | Una interfaz que almacena todas las consultas para que las utilice un objeto LibretaDirecciones. (Parte 3 de 4).

www.elsolucionario.net

25.11

173 174 175 176 177

Objetos PreparedStatement 1083

{ excepcionSql.printStackTrace(); } // fin de catch } // fin del método close } // fin de la interfaz ConsultasPersona

Figura 25.31 | Una interfaz que almacena todas las consultas para que las utilice un objeto LibretaDirecciones. (Parte 4 de 4).

En las líneas 31 y 32 se invoca el método prepareStatement de Connection para crear la instrucción PreparedStatement llamada seleccionarTodasLasPersonas, la cual selecciona todas las filas en la tabla Direcciones. En las líneas 35 y 36 se crea la instrucción PreparedStatement llamada seleccionarPersonasPorApellido con un parámetro. Esta instrucción selecciona todas las filas en la tabla Direcciones que coincidan con un apellido específico. Observe el carácter ? que se utiliza para especificar el parámetro apellido. En las líneas 39 a 42 se crea la instrucción PreparedStatement llamada insertarNuevaPersona, concuatro parámetros que representan el primer nombre, apellido paterno, dirección de e-mail y número telefónico para una nueva entrada. De nuevo, observe los caracteres ? que se utilizan para representar estos parámetros. El método obtenerTodasLasPersonas (líneas 52 a 91) ejecuta la instrucción PreparedStatement seleccionarTodasLasPersonas (línea 60) mediante una llamada al método executeQuery, el cual devuelve un objeto ResultSet que contiene las filas que coinciden con la consulta (en este caso, todas las filas en la tabla Direcciones). En las líneas 61 a 71 se colocan los resultados de la consulta en un objeto ArrayList de objetos Persona, el cual se devuelve en la línea 90 al método que hizo la llamada. El método obtenerPersonasPorApellido (líneas 95 a 137) utiliza el método setString de PreparedStatement para establecer el parámetro en seleccionarPersonasPorApellido. Después, en la línea 105 se ejecuta la consulta y en las líneas 107 a 117 se colocan los resultados de la consulta en un objeto ArrayList de objetos Persona. En la línea 136 se devuelve el objeto ArrayList al método que hizo la llamada. El método agregarPersona (líneas 140 a 163) utiliza el método setString de PreparedStatement (líneas 148 a 151) para establecer los parámetros para la instrucción PreparedStatement llamada insertarNuevaPersona. La línea 154 utiliza el método executeUpdate de PreparedStatement para insertar un nuevo registro. Este método devuelve un entero, el cual indica el número de filas que se actualizaron (o insertaron) en la base de datos. El método close (líneas 166 a 176) simplemente cierra la conexión a la base de datos.

La clase MostrarLibretaDirecciones La aplicación MostrarLibretaDirecciones (figura 25.32) utiliza un objeto de la clase ConsultasPersona para interactuar con la base de datos. En la línea 59 se crea el objeto ConsultasPersona que se utiliza en la clase MostrarLibretaDirecciones. Cuando el usuario oprime el objeto JButton llamado Explorar todas las entradas, se hace una llamada al manejador botonExplorarActionPerformed (líneas 309 a 335). En la línea 313 se hace una llamada al método obtenerTodasLasPersonas en el objeto ConsultasPersona para obtener todas las entradas en la base de datos. Después, el usuario puede desplazarse a través de las entradas, usando los objetos JButton Anterior y Siguiente. Cuando el usuario oprime el objeto JButton Buscar, se hace una llamada al manejador botonConsultaActionPerformed (líneas 265 a 287). En las líneas 267 y 268 se hace una llamada al método obtenerPersonasPorApellido en el objeto ConsultasPersona, para obtener las entradas en la base de datos que coincidan con el apellido paterno especificado. Si hay varias entradas de este tipo, el usuario puede desplazarse de una entrada a otra mediante los objetos JButton Anterior y Siguiente. Para agregar una nueva entrada a la base de datos LibretaDirecciones, el usuario puede escribir el primer nombre, apellido paterno, email y número telefónico (el valor de IdDireccion se autoincrementará) en los objetos JTextField y oprimir el objeto JButton Insertar nueva entrada. Cuando el usuario oprime este botón, se hace una llamada al manejador botonInsertarActionPerformed (líneas 338 a 352). En las líneas 340 a 342 se hace una llamada al método agregarPersona en el objeto ConsultasPersona, para agregar una nueva entrada a la base de datos. Después, el usuario puede ver distintas entradas oprimiendo los objetos JButton Anterior o Siguiente, lo cual produce llamadas a los métodos botonAnteriorActionPerformed (líneas 241 a 250) o botonSiguien-

www.elsolucionario.net

1084 Capítulo 25

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

Acceso a bases de datos con JDBC

// Fig. 25.32: MostrarLibretaDirecciones.java // Una libreta de direcciones simple import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.FlowLayout; import java.awt.GridLayout; import java.util.List; import javax.swing.JButton; import javax.swing.Box; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.WindowConstants; import javax.swing.BoxLayout; import javax.swing.BorderFactory; import javax.swing.JOptionPane; public class MostrarLibretaDirecciones extends JFrame { private Persona entradaActual; private ConsultasPersona consultasPersona; private List< Persona > resultados; private int numeroDeEntradas = 0; private int indiceEntradaActual; private private private private private private private private private private private private private private private private private private private private private private private

JButton botonExplorar; JLabel etiquetaEmail; JTextField campoTextoEmail; JLabel etiquetaPrimerNombre; JTextField campoTextoPrimerNombre; JLabel etiquetaID; JTextField campoTextoID; JTextField campoTextoIndice; JLabel etiquetaApellidoPaterno; JTextField campoTextoApellidoPaterno; JTextField campoTextoMax; JButton botonSiguiente; JLabel etiquetaDe; JLabel etiquetaTelefono; JTextField campoTextoTelefono; JButton botonAnterior; JButton botonConsulta; JLabel etiquetaConsulta; JPanel panelConsulta; JPanel panelNavegar; JPanel panelMostrar; JTextField campoTextoConsulta; JButton botonInsertar;

// constructor sin argumentos public MostrarLibretaDirecciones() { super( "Libreta de direcciones" ); // establece la conexión a la base de datos y las instrucciones PreparedStatement consultasPersona = new ConsultasPersona();

Figura 25.32 | Una libreta de direcciones simple. (Parte 1 de 7).

www.elsolucionario.net

25.11

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118

Objetos PreparedStatement 1085

// crea la GUI panelNavegar = new JPanel(); botonAnterior = new JButton(); campoTextoIndice = new JTextField( 2 ); etiquetaDe = new JLabel(); campoTextoMax = new JTextField( 2 ); botonSiguiente = new JButton(); panelMostrar = new JPanel(); etiquetaID = new JLabel(); campoTextoID = new JTextField( 10 ); etiquetaPrimerNombre = new JLabel(); campoTextoPrimerNombre = new JTextField( 10 ); etiquetaApellidoPaterno = new JLabel(); campoTextoApellidoPaterno = new JTextField( 10 ); etiquetaEmail = new JLabel(); campoTextoEmail = new JTextField( 10 ); etiquetaTelefono = new JLabel(); campoTextoTelefono = new JTextField( 10 ); panelConsulta = new JPanel(); etiquetaConsulta = new JLabel(); campoTextoConsulta = new JTextField( 10 ); botonConsulta = new JButton(); botonExplorar = new JButton(); botonInsertar = new JButton(); setLayout( new FlowLayout( FlowLayout.CENTER, 10, 10 ) ); setSize( 400, 300 ); setResizable( false ); panelNavegar.setLayout( new BoxLayout( panelNavegar, BoxLayout.X_AXIS ) ); botonAnterior.setText( "Anterior" ); botonAnterior.setEnabled( false ); botonAnterior.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent evt ) { botonAnteriorActionPerformed( evt ); } // fin del método actionPerformed } // fin de la clase interna anónima ); // fin de la llamada a addActionListener panelNavegar.add( botonAnterior ); panelNavegar.add( Box.createHorizontalStrut( 10 ) ); campoTextoIndice.setHorizontalAlignment( JTextField.CENTER ); campoTextoIndice.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent evt ) { campoTextoIndiceActionPerformed( evt ); } // fin del método actionPerformed } // fin de la clase interna anónima ); // fin de la llamada a addActionListener

Figura 25.32 | Una libreta de direcciones simple. (Parte 2 de 7).

www.elsolucionario.net

1086 Capítulo 25

119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177

Acceso a bases de datos con JDBC

panelNavegar.add( campoTextoIndice ); panelNavegar.add( Box.createHorizontalStrut( 10 ) ); etiquetaDe.setText( "de" ); panelNavegar.add( etiquetaDe ); panelNavegar.add( Box.createHorizontalStrut( 10 ) ); campoTextoMax.setHorizontalAlignment( JTextField.CENTER ); campoTextoMax.setEditable( false ); panelNavegar.add( campoTextoMax ); panelNavegar.add( Box.createHorizontalStrut( 10 ) ); botonSiguiente.setText( "Siguiente" ); botonSiguiente.setEnabled( false ); botonSiguiente.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent evt ) { botonSiguienteActionPerformed( evt ); } // fin del método actionPerformed } // fin de la clase interna anónima ); // fin de la llamada a addActionListener panelNavegar.add( botonSiguiente ); add( panelNavegar ); panelMostrar.setLayout( new GridLayout( 5, 2, 4, 4 ) ); etiquetaID.setText( "ID Direccion:" ); panelMostrar.add( etiquetaID ); campoTextoID.setEditable( false ); panelMostrar.add( campoTextoID ); etiquetaPrimerNombre.setText( "Primer nombre:" ); panelMostrar.add( etiquetaPrimerNombre ); panelMostrar.add( campoTextoPrimerNombre ); etiquetaApellidoPaterno.setText( "Apellido paterno:" ); panelMostrar.add( etiquetaApellidoPaterno ); panelMostrar.add( campoTextoApellidoPaterno ); etiquetaEmail.setText( "Email:" ); panelMostrar.add( etiquetaEmail ); panelMostrar.add( campoTextoEmail ); etiquetaTelefono.setText( "Telefono:" ); panelMostrar.add( etiquetaTelefono ); panelMostrar.add( campoTextoTelefono ); add( panelMostrar ); panelConsulta.setLayout( new BoxLayout( panelConsulta, BoxLayout.X_AXIS) ); panelConsulta.setBorder( BorderFactory.createTitledBorder( "Buscar una entrada por apellido" ) );

Figura 25.32 | Una libreta de direcciones simple. (Parte 3 de 7).

www.elsolucionario.net

25.11

178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236

Objetos PreparedStatement 1087

etiquetaConsulta.setText( "Apellido paterno:" ); panelConsulta.add( Box.createHorizontalStrut( 5 ) ); panelConsulta.add( etiquetaConsulta ); anelConsulta.add( Box.createHorizontalStrut( 10 ) ); panelConsulta.add( campoTextoConsulta ); panelConsulta.add( Box.createHorizontalStrut( 10 ) ); botonConsulta.setText( "Buscar" ); botonConsulta.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent evt ) { botonConsultaActionPerformed( evt ); } // fin del método actionPerformed } // fin de la clase interna anónima ); // fin de la llamada a addActionListener panelConsulta.add( botonConsulta ); panelConsulta.add( Box.createHorizontalStrut( 5 ) ); add( panelConsulta ); botonExplorar.setText( "Explorar todas las entradas" ); botonExplorar.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent evt ) { botonExplorarActionPerformed( evt ); } // fin del método actionPerformed } // fin de la clase interna anónima ); // fin de la llamada a addActionListener add( botonExplorar ); botonInsertar.setText( "Insertar nueva entrada" ); botonInsertar.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent evt ) { botonInsertarActionPerformed( evt ); } // fin del método actionPerformed } // fin de la clase interna anónima ); // fin de la llamada a addActionListener add( botonInsertar ); addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent evt ) { consultasPersona.close(); // cierra la conexión a la base de datos System.exit( 0 ); } // fin del método windowClosing } // fin de la clase interna anónima ); // fin de la llamada a addWindowListener

Figura 25.32 | Una libreta de direcciones simple. (Parte 4 de 7).

www.elsolucionario.net

1088 Capítulo 25

237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295

Acceso a bases de datos con JDBC

setVisible( true ); } // fin del constructor sin argumentos // maneja la llamada cuando se hace clic en botonAnterior private void botonAnteriorActionPerformed( ActionEvent evt ) { indiceEntradaActual--; if ( indiceEntradaActual < 0 ) indiceEntradaActual = numeroDeEntradas - 1; campoTextoIndice.setText( "" + ( indiceEntradaActual + 1 ) ); campoTextoIndiceActionPerformed( evt ); } // fin del método botonAnteriorActionPerformed // maneja la llamada cuando se hace clic en botonSiguiente private void botonSiguienteActionPerformed( ActionEvent evt ) { indiceEntradaActual++; if ( indiceEntradaActual >= numeroDeEntradas ) indiceEntradaActual = 0; campoTextoIndice.setText( "" + ( indiceEntradaActual + 1 ) ); campoTextoIndiceActionPerformed( evt ); } // fin del método botonSiguienteActionPerformed // maneja la llamada cuando se hace clic en botonConsulta private void botonConsultaActionPerformed( ActionEvent evt ) { resultados = consultasPersona.obtenerPersonasPorApellido( campoTextoConsulta.getText() ); numeroDeEntradas = resultados.size(); if ( numeroDeEntradas != 0 ) { indiceEntradaActual = 0; entradaActual = resultados.get( indiceEntradaActual ); campoTextoID.setText( "" + entradaActual.obtenerIDDireccion() ); campoTextoPrimerNombre.setText( entradaActual.obtenerPrimerNombre() ); campoTextoApellidoPaterno.setText( entradaActual.obtenerApellidoPaterno() ); campoTextoEmail.setText( entradaActual.obtenerEmail() ); campoTextoTelefono.setText( entradaActual.obtenerNumeroTelefonico() ); campoTextoMax.setText( "" + numeroDeEntradas ); campoTextoIndice.setText( "" + ( indiceEntradaActual + 1 ) ); botonSiguiente.setEnabled( true ); botonAnterior.setEnabled( true ); } // fin de if else botonExplorarActionPerformed( evt ); } // fin del método botonConsultaActionPerformed // maneja la llamada cuando se introduce un nuevo valor en campoTextoIndice private void campoTextoIndiceActionPerformed( ActionEvent evt ) { indiceEntradaActual = ( Integer.parseInt( campoTextoIndice.getText() ) - 1 ); if ( numeroDeEntradas != 0 && indiceEntradaActual < numeroDeEntradas )

Figura 25.32 | Una libreta de direcciones simple. (Parte 5 de 7).

www.elsolucionario.net

25.11

296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354

Objetos PreparedStatement 1089

{ entradaActual = resultados.get( indiceEntradaActual ); campoTextoID.setText("" + entradaActual.obtenerIDDireccion() ); campoTextoPrimerNombre.setText( entradaActual.obtenerPrimerNombre() ); campoTextoApellidoPaterno.setText( entradaActual.obtenerApellidoPaterno() ); campoTextoEmail.setText( entradaActual.obtenerEmail() ); campoTextoTelefono.setText( entradaActual.obtenerNumeroTelefonico() ); campoTextoMax.setText( "" + numeroDeEntradas ); campoTextoIndice.setText( "" + ( indiceEntradaActual + 1 ) ); } // fin de if } // fin del método campoTextoIndiceActionPerformed // maneja la llamada cuando se hace clic en botonExplorar private void botonExplorarActionPerformed( ActionEvent evt ) { try { resultados = consultasPersona.obtenerTodasLasPersonas(); numeroDeEntradas = resultados.size(); if ( numeroDeEntradas != 0 ) { indiceEntradaActual = 0; entradaActual = resultados.get( indiceEntradaActual ); campoTextoID.setText( "" + entradaActual.obtenerIDDireccion() ); campoTextoPrimerNombre.setText( entradaActual.obtenerPrimerNombre() ); campoTextoApellidoPaterno.setText( entradaActual.obtenerApellidoPaterno() ); campoTextoEmail.setText( entradaActual.obtenerEmail() ); campoTextoTelefono.setText( entradaActual.obtenerNumeroTelefonico() ); campoTextoMax.setText( "" + numeroDeEntradas ); campoTextoIndice.setText( "" + ( indiceEntradaActual + 1 ) ); botonSiguiente.setEnabled( true ); botonAnterior.setEnabled( true ); } // fin de if } // fin de try catch ( Exception e ) { e.printStackTrace(); } // fin de catch } // fin del método botonExplorarActionPerformed // maneja la llamada cuando se hace clic en botonInsertar private void botonInsertarActionPerformed( ActionEvent evt ) { int resultado = consultasPersona.agregarPersona( campoTextoPrimerNombre.getText(), campoTextoApellidoPaterno.getText(), campoTextoEmail.getText(), campoTextoTelefono.getText() ); if ( resultado == 1 ) JOptionPane.showMessageDialog( this, "Se agrego persona!", "Se agrego persona", JOptionPane.PLAIN_MESSAGE ); else JOptionPane.showMessageDialog( this, "No se agrego persona!", "Error", JOptionPane.PLAIN_MESSAGE ); botonExplorarActionPerformed( evt ); } // fin del método botonInsertarActionPerformed // método main

Figura 25.32 | Una libreta de direcciones simple. (Parte 6 de 7).

www.elsolucionario.net

1090 Capítulo 25

355 356 357 358 359

Acceso a bases de datos con JDBC

public static void main( String args[] ) { new MostrarLibretaDirecciones(); } // fin del método main } // fin de la clase MostrarLibretaDireccione

Figura 25.32 | Una libreta de direcciones simple. (Parte 7 de 7). teActionPerformed (líneas 253 a 262), respectivamente. De manera alternativa, el usuario número en el objeto campoTextoIndice y oprimir Intro para ver una entrada específica.

puede escribir un

25.12 Procedimientos almacenados Muchos sistemas de administración de bases de datos pueden almacenar instrucciones de SQL individuales o conjuntos de instrucciones de SQL en una base de datos, para que los programas que tengan acceso a esa base de datos puedan invocar esas instrucciones. A dichas instrucciones de SQL se les conoce como procedimientos

www.elsolucionario.net

25.14

Conclusión 1091

almacenados. JDBC permite a los programas invocar procedimientos almacenados mediante el uso de objetos que implementen a la interfaz CallableStatement. Los objetos CallableStatement pueden recibir argumentos especificados con los métodos heredados de la interfaz PreparedStatement. Además, los objetos CallableStatement pueden especificar parámetros de salida, en los cuales un procedimiento almacenado puede colocar valores de retorno. La interfaz CallableStatement incluye métodos para especificar cuáles parámetros en un procedimiento almacenado son parámetros de salida. La interfaz también incluye métodos para obtener los valores de los parámetros de salida devueltos de un procedimiento almacenado.

Tip de portabilidad 25.6 Aunque la sintaxis para crear procedimientos almacenados difiere entre los diversos sistemas de administración de bases de datos, la interfaz CallableStatement proporciona una interfaz uniforme para especificar los parámetros de entrada y salida de los procedimientos almacenados, así como para invocarlos.

Tip de portabilidad 25.7 De acuerdo con la documentación de la API para la interfaz CallableStatement, para lograr una máxima portabilidad entre los sistemas de bases de datos, los programas deben procesar las cuentas de actualización o los objetos ResultSet devueltos de un objeto CallableStatement antes de obtener los valores de cualquier parámetro de salida.

25.13 Procesamiento de transacciones Muchas aplicaciones de bases de datos requieren garantías de que una serie de operaciones de inserción, actualización y eliminación se ejecuten de manera apropiada, antes de que la aplicación continúe procesando la siguiente operación en la base de datos. Por ejemplo, al transferir dinero por medios electrónicos entre dos cuentas de banco, varios factores determinan si la transacción fue exitosa. Empezamos por especificar la cuenta de origen y el monto que deseamos transferir de esa cuenta hacia una cuenta de destino. Después, especificamos la cuenta de destino. El banco comprueba la cuenta de origen para determinar si hay suficientes fondos en ella como para poder completar la transferencia. De ser así, el banco retira el monto especificado de la cuenta de origen y, si todo sale bien, deposita el dinero en la cuenta de destino para completar la transferencia. ¿Qué ocurre si la transferencia falla después de que el banco retira el dinero de la cuenta de origen? En un sistema bancario apropiado, el banco vuelve a depositar el dinero en la cuenta de origen. ¿Cómo se sentiría usted si el dinero se restara de su cuenta de origen y el banco no depositara el dinero en la cuenta de destino? El procesamiento de transacciones permite a un programa que interactúa con una base de datos tratar una operación en la base de datos (o un conjunto de operaciones) como una sola operación. Dicha operación también se conoce como operación atómica o transacción. Al final de una transacción, se puede tomar una de dos decisiones: confirmar (commit) la transacción o rechazar (roll back) la transacción. Al confirmar la transacción se finaliza(n) la(s) operación(es) de la base de datos; todas las inserciones, actualizaciones y eliminaciones que se llevaron a cabo como parte de la transacción no pueden invertirse sin tener que realizar una nueva operación en la base de datos. Al rechazar la transacción, la base de datos queda en el estado anterior a la operación. Esto es útil cuando una parte de una transacción no se completa en forma apropiada. En nuestra discusión acerca de la transferencia entre cuentas bancarias, la transacción se rechazaría si el depósito no pudiera realizarse en la cuenta de destino. Java ofrece el procesamiento de transacciones a través de varios métodos de la interfaz Connection. El método setAutoCommit especifica si cada instrucción SQL se confirma una vez completada (un argumento true), o si deben agruparse varias instrucciones SQL para formar una transacción (un argumento false). Si el argumento para setAutoCommit es false, el programa debe colocar después de la última instrucción SQL en la transacción una llamada al método commit de Connection (para confirmar los cambios en la base de datos) o al método rollback de Connection (para regresar la base de datos al estado anterior a la transacción). La interfaz Connection también cuenta con el método getAutoCommit para determinar el estado de autoconfirmación para el objeto Connection.

25.14 Conclusión En este capítulo aprendió acerca de los conceptos básicos de las bases de datos, cómo interactuar con los datos en una base de datos mediante el uso de SQL y cómo usar JDBC para permitir que las aplicaciones de Java manipu-

www.elsolucionario.net

1092 Capítulo 25

Acceso a bases de datos con JDBC

len bases de datos MySQL y Java DB. Aprendió acerca de los comandos SELECT, INSERT, UPDATE y DELETE de SQL, y también acerca de las cláusulas como WHERE, ORDER BY e INNER JOIN. Conoció los pasos explícitos para obtener una conexión (Connection) a la base de datos, crear una instrucción (Statement) para interactuar con los datos de la base de datos, ejecutar la instrucción y procesar los resultados. Después, vio cómo usar un objeto RowSet para simplificar el proceso de conectarse a una base de datos y crear instrucciones. Utilizó instrucciones PreparedStatement para crear instrucciones de SQL precompiladas. Aprendió también a crear y configurar bases de datos, tanto en MySQL como en Java DB. También vimos las generalidades acerca de las instrucciones CallableStatement y el procesamiento de transacciones. En el siguiente capítulo, aprenderá acerca del desarrollo de aplicaciones Web con JavaServer Faces. Las aplicaciones basadas en Web crean contenido que, por lo general, se muestra en los clientes exploradores Web. Como veremos en el capítulo 27, las aplicaciones Web también pueden usar la API JDBC para acceder a las bases de datos y crear contenido Web más dinámico.

25.15 Recursos Web y lecturas recomendadas java.sun.com/products/jdbc

La página inicial sobre JDBC de Sun Microsystems, Inc. java.sun.com/docs/books/tutorial/jdbc/index.html

La trayectoria del Tutorial de Java sobre JDBC. industry.java.sun.com/products/jdbc/drivers

El motor de búsqueda de Sun Microsystems para localizar controladores de JDBC. www.sql.org

Este portal de SQL proporciona vínculos hacia muchos recursos, incluyendo la sintaxis de SQL, tips, tutoriales, libros, revistas, grupos de discusión, compañías con servicios de SQL, consultores de SQL y software gratuito. www.datadirect.com/developer/jdbc/topics/perfoptjdbc/index.ssp

Informe que describe cómo diseñar una buena aplicación de JDBC. java.sun.com/javase/6/docs/technotes/guides/jdbc/index.html

La documentación de la API JDBC de Sun Microsystems, Inc. java.sun.com/products/jdbc/faq.html

Las preguntas frecuentes acerca de JDBC de Sun Microsystems, Inc. www.jguru.com/faq/JDBC

Las FAQs sobre JDBC de JGuru. www.mysql.com

Este sitio es la página inicial de la base de datos MySQL. Puede descargar las versiones más recientes de MySQL y MySQL Connector/J, y acceder a su documentación en línea. www.mysql.com/products/enterprise/server.html

Introducción al servidor de bases de datos MySQL y vínculos a sus sitios de documentación y descargas. dev.mysql.com/doc/mysql/en/index.html dev.mysql.com/doc/refman/5.0/es/index.html

Manual de referencia de MySQL, en inglés y en español. dev.mysql.com/doc/refman/5.1/en/connector-mxj.html

Documentación sobre MySQL Connector/J, incluyendo instrucciones de instalación y ejemplos. developers.sun.com/prodtech/javadb/reference/docs/10.2.1.6/devguide/index.html

La Guía para el desarrollador de Java DB. java.sun.com/javase/6/docs/technotes/guides/jdbc/getstart/rowsetImpl.html Presenta las generalidades acerca de la interfaz RowSet y sus subinterfaces. Este sitio también habla sobre las implemen-

taciones de referencia de estas interfaces de Sun y su uso. developer.java.sun.com/developer/Books/JDBCTutorial/chapter5.html El capítulo 5 (tutorial de RowSet) del libro The JDBC 2.0 API Tutorial and Reference,

Segunda edición.

Lecturas recomendadas Ashmore, D. C., “Best Practices for JDBC Programming”, Java Developers Journal, 5: núm. 4 (2000), 42 a 54. Blaha, M. R., W. J. Premerlani y J. E. Rumbaugh, “Relational Database Design Using an Object-Oriented Methodology”, Communications of the ACM, 31: núm. 4 (1988): 414 a 427. Brunner, R. J., “The Evolution of Connecting”, Java Developers Journal, 5: núm. 10 (2000): 24 a 26.

www.elsolucionario.net

Resumen 1093 Brunner, R. J., “After the Connection”, Java Developers Journal, 5: núm. 11 (2000): 42 a 46. Callahan, T., “So You Want a Stand-Alone Database for Java”, Java Developers Journal, 3: núm. 12 (1998): 28 a 36. Codd, E. F. “A Relational Model of Data for Large Shared Data Banks”, Communications of the ACM, Junio 1970. Codd, E. F. “Further Normalization of the Data Base Relational Model”, Courant Computer Science Symposia, Vol. 6, Data Base Systems. Upple Saddle River, NJ: Prentice Hall, 1972. Codd, E. F. “Fatal Flaws in SQL”. Datamation, 34: núm. 16 (1988): 45 a 48. Cooper, J. W. “Making Databases Easier for Your Users”, Java Pro, 4: núm. 10 (2000): 47 a 54. Date, C. J. An Introduction to Database Systems, 8/e. Reading, MA: Pearson Education, 2003. Deitel, H. M., P. J. Deitel y D. R. Choffnes. Operating Systems, Tercera edición. Upper Saddle River, NJ: Prentice Hall, 2004. Duguay, C. “Electronic Mail Merge”. Java Pro, Invierno 1999/2000, 22 a 32. Ergul, S. “Transaction Processing with Java”. Java Report, Enero 2001, 30 a 36. Fisher, M. “JDBC Database Access” (una trayectoria en The Java Tutorial), . Harrison, G., “Browsing the JDBC API”. Java Developers Journal, 3: núm. 2 (1998): 44 a 52. Jasnowski, M., “persistente Frameworks”. Java Developers Journal, 5: núm. 11 (2000): 82 a 86. “JDBC API Documentation”. . Jordan, D. “An Overview of Sun’s Java Data Objects Specification”. Java Pro, 4: núm. 6 (2000): 102 a 108. Khanna, P. “Managing Object Persistente with JDBC”. Java Pro, 4: núm. 5 (2000): 28 a 33. Reese, G. Database Programming with JDBC and Java, Segunda edición. Cambridge, MA: O’Reilly, 2001. Spell, B. “Create Enterprise Applications with JDBC 2.0”. Java Pro, 4: núm. 4 (2000): 40 a 44. Stonebraker, M. “Operating System Support for Database Management”. Communications of the ACM, 24: núm. 7 (1981): 412 a 418. Taylor, A. JDBC Developer’s Resource: Database Programming on the Internet. Upper Saddle River, NJ: Prentice Hall, 1999. Thilmany, C. “Applying Patterns to JDBC Development”. Java Developers Journal, 5: núm. 6 (2000): 80 a 90. Venugopal, S. 2000. “Cross-Database Portability with JDBC”. Java Developers Journal, 5: núm. 1 (2000): 58-62. White, S., M. Fisher, R. Cattell, G. Hamilton y M. Hapner. JDBC API Tutorial and Reference, Segunda edición. Boston, MA: Addison Wesley, 1999. Winston, A. “A Distributed Database Primer”. UNIX World, Abril 1988, 54 a 63.

Resumen Sección 25.1 Introducción • Una base de datos es una colección integrada de datos. Un sistema de administración de bases de datos (DBMS) proporciona mecanismos para almacenar, organizar, obtener y modificar datos para muchos usuarios. • Los sistemas de administración de bases de datos más populares hoy en día son los sistemas de bases de datos relacionales. • SQL es el lenguaje estándar internacional, utilizado casi universalmente con sistemas de bases de datos relacionales, para realizar consultas y manipular datos. • Los programas en Java se conectan a (e interactúan con) bases de datos relacionales a través de una interfaz: software que facilita las comunicaciones entre un sistema de administración de bases de datos y un programa. • Los programas en Java se comunican con las bases de datos y manipulan sus datos utilizando la API JDBC. Un controlador de JDBC permite a las aplicaciones de Java conectarse a una base de datos en un DBMS específico, y nos permite obtener y manipular los datos de una base de datos.

Sección 25.2 Bases de datos relacionales • Una base de datos relacional almacena los datos en tablas. Las tablas están compuestas de filas, y las filas están compuestas de columnas en las que se almacenan los valores. • Una clave primaria proporciona un valor único que no puede duplicarse en otras filas. • Cada columna de la tabla representa un atributo distinto. • La clave primaria puede estar compuesta por más de una columna. • SQL proporciona un conjunto completo de instrucciones que permiten a los programadores definir consultas complejas para recuperar datos de una base de datos.

www.elsolucionario.net

1094 Capítulo 25

Acceso a bases de datos con JDBC

• Toda columna en una clave primaria debe tener un valor, y el valor de la clave primaria debe ser único. A esto se le conoce como la Regla de integridad de entidades. • Una relación de uno a varios entre tablas indica que una fila en una tabla puede tener muchas filas relacionadas en otra tabla. • Una clave externa es una columna en una tabla que debe coincidir con el valor de la clave primaria en otra tabla. • La clave externa ayuda a mantener la Regla de la integridad referencial: cada valor de clave externa debe aparecer como el valor de clave primaria de otra tabla. Las claves externas permiten que se una la información de varias tablas. Hay una relación de uno a varios entre una clave primaria y su correspondiente clave externa.

Sección 25.4.1 Consulta básica SELECT • La forma básica de una consulta SELECT es: SELECT * FROM

nombreDeTabla

en donde el asterisco (*) indica que deben seleccionarse todas las columnas de nombreDeTabla y nombreDeTabla especifica la tabla en la base de datos de la cual se van a recuperar los datos. • Para obtener ciertas columnas de una tabla, reemplace el asterisco (*) con una lista separada por comas de los nombres de las columnas. • Los programadores procesan los resultados de una consulta conociendo de antemano el orden de las columnas en el resultado. Al especificar las columnas explícitamente, se garantiza que siempre se devuelvan en el orden especificado, incluso aunque el orden real en la(s) tabla(s) sea distinto.

Sección 25.4.2 La cláusula WHERE • La cláusula WHERE opcional en una consulta SELECT especifica los criterios de selección para la consulta. La forma básica de una consulta SELECT con criterios de selección es: SELECT

nombreDeColumna1, nombreDeColumna2, … FROM nombreDeTabla WHERE criterios

• La cláusula WHERE puede contener los operadores , =, =, y LIKE. El operador LIKE se utiliza para relacionar patrones con los caracteres comodines por ciento (%) y guión bajo (_). • Un carácter de por ciento (%) en un patrón indica que una cadena que coincida con ese patrón puede tener cero o más caracteres en la ubicación del carácter de por ciento en el patrón. • Un guión bajo (_) en la cadena del patrón indica un carácter individual en esa posición en el patrón.

Sección 25.4.3 La cláusula ORDER BY • El resultado de una consulta puede ordenarse en forma ascendente o descendente, mediante el uso de la cláusula ORDER BY opcional. La forma más simple de una cláusula ORDER BY es: SELECT SELECT

nombreDeColumna1, nombreDeColumna2, … FROM nombreDeTabla ORDER BY columna ASC nombreDeColumna1, nombreDeColumna2, … FROM nombreDeTabla ORDER BY columna DESC

en donde ASC especifica el orden ascendente, DESC especifica el orden descendente y columna especifica la columna en la cual se basa el ordenamiento. El orden predeterminado es ascendente, por lo que ASC es opcional. • Pueden usarse varias columnas para ordenar mediante una cláusula ORDER BY, de la siguiente forma: ORDER BY

columna1 tipoDeOrden, columna2 tipoDeOrden, …

• Las cláusulas WHERE y ORDER BY pueden combinarse en una consulta. Si se utiliza, la cláusula ORDER BY debe ser la última cláusula en la consulta.

Sección 25.4.4 Cómo fusionar datos de varias tablas: INNER JOIN • Un operador INNER JOIN fusiona las filas de dos tablas al relacionar los valores en columnas que sean comunes para las dos tablas. La forma básica del operador INNER JOIN es: SELECT nombreDeColumna1, nombreDeColumna2, … FROM tabla1 INNER JOIN tabla2 ON tabla1.nombreDeColumna = tabla2.nombreDeColumna

La cláusula ON especifica las columnas de cada tabla que se van a comparar para determinar cuáles filas se van a unir. Si una instrucción de SQL utiliza columnas con el mismo nombre provenientes de varias tablas, los nombres de las

www.elsolucionario.net

Resumen 1095 columnas deben estar completamente calificados y se debe colocar antes de ellos sus nombres de tablas y un operador punto (.).

Sección 25.4.5 La instrucción INSERT • Una instrucción INSERT inserta una nueva fila en una tabla. La forma básica de esta instrucción es: INSERT INTO nombreDeTabla ( nombreDeColumna1, VALUES ( valor1, valor2, ..., valorN )

nombreDeColumna2, …, nombreDeColumnaN )

en donde nombreDeTabla es la tabla en la que se va a insertar la fila. El nombreDeTabla va seguido de una lista separada por comas de los nombres de columnas, entre paréntesis. La lista de nombres de columnas va seguida por la palabra clave VALUES de SQL, y una lista separada por comas de valores entre paréntesis. • Las instrucciones de SQL utilizan el carácter de comilla sencilla (‘) como delimitador para las cadenas. Para especificar una cadena que contenga una comilla sencilla en una instrucción de SQL, la comilla sencilla debe escaparse con otra comilla sencilla.

Sección 25.4.6 La instrucción UPDATE • Una instrucción UPDATE modifica los datos en una tabla. La forma básica de la instrucción UPDATE es: UPDATE nombreDeTabla SET nombreDeColumna1 WHERE criterios

= valor1, nombreDeColumna2 = valor2, …, nombreDeColumnaN = valorN

en donde nombreDeTabla es la tabla de la que se van a actualizar los datos. El nombreDeTabla va seguido por la palabra clave SET y una lista separada por comas de los pares nombre/valor de las columnas, en el formato nombreDeColumna=valor. Los criterios de la cláusula WHERE determinan las filas que se van a actualizar.

Sección 25.4.7 La instrucción DELETE • Una instrucción DELETE elimina filas de una tabla. La forma más simple de una instrucción DELETE es: DELETE FROM

nombreDeTabla WHERE criterios

en donde nombreDeTabla es la tabla de la que se va a eliminar una fila (o filas). Los criterios de la cláusula WHERE opcional determinan cuál(es) fila(s) se va(n) a eliminar. Si se omite esta cláusula, se eliminan todas las filas de la tabla.

Sección 25.8.1 Cómo conectarse y realizar consultas en una base de datos • El paquete java.sql contiene clases e interfaces para manipular bases de datos relacionales en Java. • Un objeto que implementa a la interfaz Connection administra la conexión entre el programa de Java y una base de datos. Los objetos Connection permiten a los programas crear instrucciones de SQL para acceder a los datos. • El método getConnection de la clase DriverManager trata de conectarse a una base de datos especificada por su argumento URL. El URL ayuda al programa a localizar la base de datos. El URL incluye el protocolo y el subprotocolo de comunicación, junto con el nombre de la base de datos. • El método createStatement de Connection crea un objeto de tipo Statement. El programa utiliza al objeto Statement para enviar instrucciones de SQL a la base de datos. • El método executeQuery de Statement ejecuta una consulta y devuelve un objeto que implementa a la interfaz ResultSet que contiene el resultado de la consulta. Los métodos de ResultSet permiten a un programa manipular el resultado de la consulta. • Un objeto ResultSetMetaData describe el contenido de un objeto ResultSet. Los programas pueden usar metadatos mediante la programación, para obtener información acerca de los nombres y tipos de las columnas del objeto ResultSet. • El método getColumnCount de ResultSetMetaData recupera el número de columnas en el objeto ResultSet. • El método next de ResultSet posiciona el cursor de ResultSet en la siguiente fila del objeto ResultSet. El cursor apunta a la fila actual. El método next devuelve el valor boolean true si el cursor puede posicionarse en la siguiente fila; en caso contrario el método devuelve false. Este método debe llamarse para empezar a procesar un objeto ResultSet. • Al procesar objetos ResultSet, es posible extraer cada columna del objeto ResultSet como un tipo de Java específico. El método getColumnType de ResultSetMetaData devuelve un valor entero constante de la clase Types (paquete java.sql), indicando el tipo de los datos de una columna específica.

www.elsolucionario.net

1096 Capítulo 25

Acceso a bases de datos con JDBC

• Los métodos obtener (get) de ResultSet generalmente reciben como argumento un número de columna (como un valor int) o un nombre de columna (como un valor String), indicando cuál valor de la columna se va a obtener. • Los números de fila y columna de un objeto ResultSet empiezan en 1. • Cada objeto Statement puede abrir solamente un objeto ResultSet en un momento dado. Cuando un objeto Statement devuelve un nuevo objeto ResultSet, el objeto Statement cierra el objeto ResultSet anterior. • El método createStatement de Connection tiene una versión sobrecargada que toma dos argumentos: el tipo y la concurrencia del resultado. El tipo del resultado especifica si el cursor del objeto ResultSet puede desplazarse en ambas direcciones o solamente hacia delante, y si el objeto ResultSet es susceptible a los cambios. La concurrencia del resultado especifica si el objeto ResultSet puede actualizarse con los métodos de actualización de ResultSet. • Algunos controladores JDBC no soportan objetos ResultSet desplazables y/o actualizables.

Sección 25.8.2 Consultas en la base de datos libros • El método getColumnClass de TableModel devuelve un objeto Class que representa a la superclase de todos los objetos en una columna específica. El objeto JTable utiliza esta información para configurar el desplegador de celdas y el editor de celdas predeterminados para esa columna en un objeto JTable. • El método getColumnClassName de ResultSetMetaData obtiene el nombre completamente calificado de la columna especificada. • El método getColumnCount de TableModel devuelve el número de columnas en el objeto ResultSet subyacente del modelo. • El método getColumnName de TableModel devuelve el nombre de la columna en el objeto ResultSet subyacente del modelo. • El método getColumnName de ResultSetMetaData obtiene el nombre de la columna proveniente del objeto ResultSet. • El método getRowCount de TableModel devuelve el número de filas en el objeto ResultSet subyacente del modelo. • El método getValueAt de TableModel devuelve el objeto Object en una fila y columna específicas del objeto Result-Set subyacente del modelo. • El método absolute de ResultSet posiciona el cursor de ResultSet en una fila específica. • El método fireTableStructureChanged de AbstractTableModel notifica a cualquier objeto JTable que utilice un objeto TableModel específico como su modelo, que los datos en el modelo han cambiado.

Sección 25.9 La interfaz RowSet • La interfaz RowSet configura la conexión a la base de datos y ejecuta la consulta en forma automática. • Hay dos tipos de objetos RowSet: conectados y desconectados. • Un objeto RowSet conectado se conecta a la base de datos una vez, y permanece conectado hasta que termina la aplicación. • Un objeto RowSet desconectado se conecta a la base de datos, ejecuta una consulta para obtener los datos de la base de datos y después cierra la conexión. • JdbcRowSet, un objeto RowSet conectado, actúa como envoltura para un objeto ResultSet y nos permite desplazar y actualizar las filas en el objeto ResultSet. A diferencia de un objeto ResultSet, un objeto JdbcRowSet puede desplazarse y actualizarse de manera predeterminada. • CachedRowSet, un objeto RowSet desconectado, coloca en caché de memoria los datos de un objeto ResultSet. Al igual que JdbcRowSet, un objeto CachedRowSet puede desplazarse y actualizarse. Un objeto CachedRowSet también es serializable, por lo que se puede pasar entre aplicaciones de Java a través de una red, como Internet.

Sección 25.10 Java DB/Apache Derby • A partir del JDK 6.0, Sun Microsystems incluye la base de datos de código fuente abierto, basada exclusivamente en Java, llamada Java DB (la versión producida por Sun de Apache Derby) junto con el JDK. • Java DB tiene una versión incrustada y una versión en red.

Sección 25.11 Objetos PreparedStatement • La interfaz PreparedStatement nos permite crear instrucciones de SQL compiladas, que se ejecutan con más eficiencia que los objetos Statement. • Las instrucciones PreparedStatement también pueden especificar parámetros, lo cual las hace más flexibles que las instrucciones Statement. Los programas pueden ejecutar la misma consulta varias veces, con distintos valores para los parámetros.

www.elsolucionario.net

Terminología 1097 • Un parámetro se especifica mediante un signo de interrogación (?) en la instrucción SQL. Antes de ejecutar una instrucción PreparedStatement, el programa debe especificar los valores de los parámetros mediante el uso de los métodos establecer (set) de la interfaz PreparedStatement. • El primer argumento del método setString de PreparedStatement representa el número del parámetro que se va a establecer, y el segundo argumento es el valor de ese parámetro. • Los números de los parámetros se cuentan a partir de 1, empezando con el primer signo de interrogación (?). • El método setString escapa de manera automática los valores de los parámetros String, según sea necesario. • La interfaz PreparedStatement proporciona métodos establecer para cada tipo de SQL soportado. • Una columna de identidad es la manera estándar en SQL de representar una columna autoincrementada. La palabra clave IDENTITY de SQL se utiliza para marcar una columna como columna de identidad.

Sección 25.12 Procedimientos almacenados • JDBC permite a los programas invocar procedimientos almacenados mediante el uso de objetos que implementen a la interfaz CallableStatement. • Los objetos CallableStatement pueden especificar parámetros de entrada, como PreparedStatement. Además, los objetos CallableStatement pueden especificar parámetros de salida, en los cuales un procedimiento almacenado puede colocar valores de retorno.

Sección 25.13 Procesamiento de transacciones • El procesamiento de transacciones permite a un programa que interactúa con una base de datos, tratar una operación en la base de datos (o un conjunto de operaciones) como una sola operación. Dicha operación se conoce también como una operación atómica de una transacción. • Al final de una transacción, se puede tomar una de dos decisiones: confirmar la transacción o rechazarla. • Al confirmar la transacción se finaliza(n) toda(s) la(s) operación(es) en la base de datos; todas las inserciones, actualizaciones y eliminaciones que se llevan a cabo como parte de la transacción no pueden invertirse sin realizar una operación nueva en la base de datos. • Al rechazar una transacción, la base de datos queda en el estado anterior al de la operación. Esto es útil cuando una parte de una transacción no se completa en forma apropiada. • Java proporciona el procesamiento de transacciones a través de los métodos de la interfaz Connection. • El método setAutoCommit especifica si cada instrucción de SQL se confirma al momento de completarse (un argumento true), o si deben agruparse varias instrucciones de SQL como una transacción (un argumento false). • Si el argumento para setAutoCommit es false, el programa debe colocar, después la última instrucción SQL en la transacción, una llamada al método commit de Connection (para confirmar los cambios a la base de datos), o al método rollback de Connection (para regresar la base de datos al estado anterior a la transacción). • El método getAutoCommit determina el estado de autoconfirmación para el objeto Connection.

Terminología %, carácter comodín de SQL _, carácter comodín de SQL *, carácter comodín de SQL absolute, método de ResultSet AbstractTableModel, clase addTableModelListener, método Model

base de datos base de datos relacional CachedRowSet, interfaz CallableStatement, interfaz clave externa clave primaria close, método de Connection close, método de Statement coincidencia de parámetros columna com.mysql.jdbc.Driver

de la interfaz Table-

conectarse a una base de datos Connection, interfaz consultar una base de datos controlador de JDBC createStatement, método de la interfaz Connection criterios de selección DELETE, instrucción de SQL deleteRow, método de ResultSet descubrimiento automático de controladores DriverManager, clase execute, método de la interfaz JdbcRowSet execute, método de la interfaz Statement executeQuery, método de la interfaz Statement executeUpdate, método de la interfaz Statement Fila filtrar los datos en un objeto TableModel fireTableStructureChanged, método de la clase AbstractTableModel

www.elsolucionario.net

1098 Capítulo 25

Acceso a bases de datos con JDBC

getColumnClass, método de la interfaz TableModel getColumnClassName, método de la interfaz ResultSetMetaData getColumnCount, método de la interfaz ResultSetMetaData getColumnCount, método de la interfaz TableModel getColumnName, método de la interfaz ResultSetMetaData getColumnName, método de la interfaz TableModel getColumnType, método de la interfaz ResultSetMetaData getConnection, método de la clase DriverManager getMetaData, método de la interfaz ResultSet getMoreResults, método de la interfaz Statement getObject, método de la interfaz ResultSet getResultSet, método de la interfaz Statement getRow, método de la interfaz ResultSet getRowCount, método de la interfaz TableModel getUpdateCount, método de la interfaz Statement getValueAt, método de la interfaz TableModel INNER JOIN, operador de SQL INSERT, instrucción de SQL insertRow, método de la interfaz ResultSet java.sql, paquete javax.sql, paquete javax.sql.rowset, paquete javax.swing.table, paquete

JDBC JdbcRowSet, interfaz JdbcRowSetImpl, clase last, método de la interfaz ResultSet

objeto RowSet desconectado ON, cláusula ordenamiento de filas ORDER BY, cláusula parámetro de salida PreparedStatement, interfaz procedimiento almacenado regexFilter, método de la clase RowFilter Regla de integridad de entidades Regla de integridad referencial relación de uno a varios removeTableModelListener, método de la interfaz TableModel

resultado ResultSet, interfaz ResultSetMetaData, RowFilter, clase

interfaz

secuencia de comandos de SQL SELECT, palabra clave de SQL setCommand, método de la interfaz JdbcRowSet setPassword, método de la interfaz JdbcRowSet setRowFilter, método de la clase JTable setRowSorter, método de la clase JTable setString, método de la interfaz PreparedStatement setUrl, método de la interfaz JdbcRowSet setUsername, método de la interfaz JdbcRowSet

SQL (Lenguaje de consulta estructurado) SQLException, clase Statement, interfaz sun.jdbc.odbc.JdbcOdbcDriver

tabla

metadatos moveToCurrentRow, método de ResultSet moveToInsertRow, método de ResultSet

MySQL Connector/J MySQL, base de datos next, método de la interfaz ResultSet objeto ResultSet actualizable objeto RowSet conectado

TableModel, interfaz TableModelEvent, clase TableRowSorter, clase Types, clase

unir UPDATE, instrucción de SQL updateRow, método de la interfaz ResultSet WHERE, cláusula de una instrucción de SQL

Ejercicios de autoevaluación 25.1

Complete las siguientes oraciones: a) El lenguaje de consulta de bases de datos estándar internacional es ______________. b) Una tabla en una base de datos consiste de ______________ y ______________. c) Los objetos Statement devuelven los resultados de una consulta de SQL como objetos ______________. d) La ______________ identifica en forma única a cada fila en una tabla. e) La palabra clave ______________ de SQL va seguida por los criterios de selección que especifican las filas a seleccionar en una consulta. f ) Las palabras clave ______________ de SQL especifican la manera en que se ordenan las filas en una consulta. g) Al proceso de fusionar filas de varias tablas de una base de datos se le conoce como ______________ las tablas. h) Una ______________ es una colección organizada de datos. i) Una ______________ es un conjunto de columnas cuyos valores coinciden con los valores de clave primaria de otra tabla.

www.elsolucionario.net

Ejercicios 1099 j) Un objeto ______________ se utiliza para obtener una conexión (Connection) a una base de datos. k) La interfaz ______________ ayuda a administrar la conexión entre un programa de Java y una base de datos. l) Un objeto ______________ se utiliza para enviar una consulta a una base de datos. m) A diferencia de un objeto ResultSet, los objetos ______________, ______________ y _____________ son desplazables y se pueden actualizar de manera predeterminada. n) ______________, un objeto RowSet desconectado, coloca en caché los datos de un objeto ResultSet en la memoria.

Respuestas a los ejercicios de autoevaluación 25.1 a) SQL. b) filas, columnas. c) ResultSet. d) clave primaria. e) WHERE. f ) ORDER BY. g) unir. h) base de datos. i) clave externa. j) DriverManager. k) Connection. l) Statement. m) JdbcRowSet, CachedRowSet. n) CachedRowSet.

Ejercicios 25.2 Utilizando las técnicas mostradas en este capítulo, defina una aplicación de consulta completa para la base de datos libros. Proporcione las siguientes consultas predefinidas: a) Seleccionar todos los autores de la tabla autores. b) Seleccionar un autor específico y mostrar todos los libros de ese autor. Incluir el título, año e ISBN. Ordenar la información alfabéticamente, en base al apellido paterno y nombre de pila del autor. c) Seleccionar una editorial específica y mostrar todos los libros publicados por esa editorial. Incluir el título, año e ISBN. Ordenar la información alfabéticamente por título. d) Proporcione cualquier otra consulta que usted crea que sea apropiada. Muestre un objeto JComboBox con nombres apropiados para cada consulta predefinida. Además, permita a los usuarios suministrar sus propias consultas. 25.3 Defina una aplicación de manipulación de bases de datos para la base de datos libros. El usuario deberá poder editar los datos existentes y agregar nuevos datos a la base de datos (obedeciendo las restricciones de integridad referencial y de entidades). Permita al usuario editar la base de datos de las siguientes formas: a) Agregar un nuevo autor. b) Editar la información existente para un autor. d) Agregar un nuevo título para un autor. (Recuerde que el libro debe tener una entrada en la tabla isbnAutor). d) Agregar una nueva entrada en la tabla isbnAutor para enlazar los autores con los títulos. 25.4 En la sección 10.7 presentamos una jerarquía nómina-empleado para calcular la nómina de cada empleado. En este ejercicio proporcionamos una base de datos de empleados que corresponde a la jerarquía nómina-empleado. (En el CD que acompaña a este libro y en nuestro sitio Web www.deitel.com, se proporciona una secuencia de comandos de SQL para crear la base de datos de empleados, junto con los ejemplos de este capítulo). Escriba una aplicación que permita al usuario: a) Agregar empleados a la tabla empleado. b) Para cada nuevo empleado, agregar información de la nómina a la tabla correspondiente. Por ejemplo, para un empleado asalariado agregue la información de nómina a la tabla empleadosAsalariados. La figura 25.33 es el diagrama de relaciones de entidades para la base de datos empleados. 25.5 Modifique el ejercicio 25.4 para proporcionar un objeto JComboBox y un objeto JTextArea para permitir al usuario realizar una consulta que se seleccione del objeto JComboBox, o que se defina en el objeto JTextArea. Las consultas predefinidas de ejemplo son: a) Seleccionar a todos los empleados que trabajen en el departamento de VENTAS. b) Seleccionar a los empleados por horas que trabajen más de 30 horas. c) Seleccionar a todos los empleados por comisión en orden descendente en base a la tasa de comisión. 25.6

Modifique el ejercicio 25.5 para realizar las siguientes tareas: a) Incrementar el salario base en un 10% para todos los empleados base más comisión. b) Si el cumpleaños del empleado es en el mes actual, agregar un bono de $100. c) Para todos los empleados por comisión cuyas ventas brutas sean mayores de $10,000, agregar un bono de $100.

www.elsolucionario.net

1100 Capítulo 25

Acceso a bases de datos con JDBC

25.7 Modifique el programa de las figuras 25.31 a 25.33 para proporcionar un objeto JButton que permita al usuario llamar a un método actualizarPersona en la interfaz ConsultasPersona, para actualizar la entrada actual en la base de datos LibretaDirecciones.

empleadosAsalariados

1

numeroSeguroSocial

empleadoPorComision 1

salarioSemanal bono

numeroSeguroSocial ventasBrutas tarifaComision bono

1

empleados

1

numeroSeguroSocial 1

1

primerNombre apellidoPaterno cumpleaños tipoEmpleado nombreDepartamento

empleadoBaseMasComision

empleadoPorHora numeroSeguroSocial

1

1

numeroSeguroSocial

horas

ventasBrutas

sueldo

tarifaComision

bono

salarioBase bono

Figura 25.33 | Relaciones de las tablas en empleados.

www.elsolucionario.net

26 Aplicaciones Web: parte 1 Si cualquier hombre prepara su caso y coloca su nombre al pie de la primera página, yo le daré una respuesta inmediata. Si me obliga a dar vuelta a la hoja, deberá esperar a mi conveniencia. —Lord Sandwich

Regla uno: nuestro cliente siempre tiene la razón. Regla dos: si piensas que nuestro cliente está mal, consulta la Regla uno. —Anónimo

Una pregunta justa debe ir seguida de un acto en silencio.

OBJETIVOS En este capítulo aprenderá a: Q

Desarrollar aplicaciones Web mediante el uso de las tecnologías de Java y Java Studio Creator 2.0.

Q

Crear JavaServer Pages con componentes JavaServer Faces.

Q

Crear aplicaciones Web que consistan de varias páginas.

Q

Validar la entrada del usuario en una página Web.

Q

Mantener la información de estado acerca de un usuario, con rastreo de sesión y cookies.

—Dante Alighieri

Vendrás aquí y obtendrás libros que abrirán tus ojos, oídos y tu curiosidad; y sacarán tu interior, o meterán tu exterior. —Ralph Waldo Emerson

www.elsolucionario.net

Pla n g e ne r a l

1102 Capítulo 26 Aplicaciones Web: parte 1

26.1 26.2 26.3 26.4

26.5

26.6

26.7

26.8 26.9

Introducción Transacciones HTTP simples Arquitectura de aplicaciones multinivel Tecnologías Web de Java 26.4.1 Servlets 26.4.2 JavaServer Pages 26.4.3 JavaServer Faces 26.4.4 Tecnologías Web en Java Studio Creator 2 Creación y ejecución de una aplicación simple en Java Studio Creator 2 26.5.1 Análisis de un archivo JSP 26.5.2 Análisis de un archivo de bean de página 26.5.3 Ciclo de vida del procesamiento de eventos 26.5.4 Relación entre la JSP y los archivos de bean de página 26.5.5 Análisis del XHTML generado por una aplicación Web de Java 26.5.6 Creación de una aplicación Web en Java Studio Creator 2 Componentes JSF 26.6.1 Componentes de texto y gráficos 26.6.2 Validación mediante los componentes de validación y los validadores personalizados Rastreo de sesiones 26.7.1 Cookies 26.7.2 Rastreo de sesiones con el objeto SessionBean Conclusión Recursos Web

Resumen | Terminología | Ejercicios de autoevaluación | Respuestas a los ejercicios de autoevaluación | Ejercicios

26.1 Introducción En este capítulo, presentaremos el desarrollo de aplicaciones Web con tecnología basada en Java. Las aplicaciones basadas en Web crean contenido Web para los clientes navegadores Web. Este contenido Web incluye el Lenguaje de marcado de hipertexto extensible (XHTML), las secuencias de comandos del lado servidor, imágenes y datos binarios. Para aquellos que no están familiarizados con XHTML, en el CD que se incluye con este libro hay tres capítulos de nuestro libro Internet & World Wide Web How to Program, 3/e [Introduction to XHTML: Part 1, Introduction to XHTML: Part 2 y Cascading Style Sheets (CSS)]. En los capítulos 26 a 28, vamos a suponer que usted ya sabe utilizar XHTML. Este capítulo empieza con las generalidades de la arquitectura de aplicaciones multinivel, y las tecnologías Web de Java para implementar aplicaciones multinivel. Después presentaremos varios ejemplos que demuestran el desarrollo de aplicaciones Web. El primer ejemplo lo introducirá al desarrollo Web de Java. En el segundo ejemplo, crearemos una aplicación Web que simplemente muestra la apariencia visual de varios componentes de GUI de aplicaciones Web. Después, le demostraremos cómo utilizar los componentes de validación y los métodos de validación personalizados para asegurar que la entrada del usuario sea válida antes de enviarla para que el servidor la procese. El capítulo termina con dos ejemplos acerca de cómo personalizar la experiencia de un usuario mediante el rastreo de sesiones. En el capítulo 27 continuaremos nuestra discusión acerca del desarrollo de aplicaciones Web con conceptos más avanzados, incluyendo los componentes habilitados para AJAX del modelo de programación Java BluePrints de Sun. AJAX ayuda a las aplicaciones basadas en Web a proporcionar la interactividad y capacidad de respuesta que los usuarios esperan comúnmente de las aplicaciones de escritorio. A lo largo de este capítulo y del capítulo 27 utilizaremos Sun Java Studio Creator 2.0: un IDE que ayuda al programador a crear aplicaciones Web mediante el uso de tecnologías de Java, como JavaServer Pages y JavaServer Faces. Para implementar los ejemplos que se presentan en este capítulo, debe instalar Java Studio Creator 2.0, el

www.elsolucionario.net

26.2 Transacciones HTTP simples 1103 cual está disponible para su descarga en developers.sun.com/prodtech/javatools/jscreator/downloads/ Las características de Java Studio Creator 2.0 se están incorporando en Netbeans 5.5, mediante un complemento llamado Netbeans Visual Web Pack 5.5 (www.netbeans.org/products/visualweb/).

index.jsp.

26.2 Transacciones HTTP simples El desarrollo de aplicaciones Web requiere una comprensión básica de las redes y de World Wide Web. En esta sección, hablaremos sobre el Protocolo de transferencia de hipertexto (HTTP) y lo que ocurre “tras bambalinas”, cuando un usuario solicita una página Web en un navegador. HTTP especifica un conjunto de métodos y encabezados que permiten a los clientes y servidores interactuar e intercambiar información de una manera uniforme y confiable. En su forma más simple, una página Web no es más que un documento XHTML: un archivo de texto simple que contiene marcado (es decir, etiquetas) para describir a un navegador Web cómo mostrar y dar formato a la información del documento. Por ejemplo, el siguiente marcado de XHTML: Mi pagina Web

indica que el navegador debe mostrar el texto entre la etiqueta inicial y la etiqueta final en la barra de título del navegador. Los documentos XHTML también pueden contener datos de hipertexto (lo que se conoce comúnmente como hipervínculos) que vinculan a distintas páginas, o a otras partes de la misma página. Cuando el usuario activa un hipervínculo (por lo general, haciendo clic sobre él con el ratón), la página Web solicitada se carga en el navegador Web del usuario. HTTP utiliza URIs (Identificadores uniformes de recursos) para identificar datos en Internet. Los URIs que especifican las ubicaciones de los documentos se llaman URLs (Localizadores uniformes de recursos). Los URLs comunes se refieren a archivos, directorios u objetos que realizan tareas complejas, como búsquedas en bases de datos y en Internet. Si usted conoce el URL de HTTP de un documento XHTML disponible públicamente en cualquier parte en la Web, puede acceder a este documento a través de HTTP. Un URL contiene la información que dirige a un navegador al recurso que el usuario desea utilizar. Las computadoras que ejecutan software de servidor Web hacen disponibles esos recursos. Vamos a examinar los componentes del URL: http://www.deitel.com/libros/descargas.html

El

http:// indica que el recurso se debe obtener mediante el protocolo HTTP. La porción intermedia, www.deitel.com, es el nombre del host completamente calificado del servidor: el nombre del servidor en el que

reside el recurso. Esta computadora se conoce comúnmente como host, debido a que aloja y da mantenimiento a los recursos. El nombre de host www.deitel.com se traduce en una dirección IP (68.236.123.125), la cual identifica al servidor así como un número telefónico identifica de forma única a una línea telefónica específica. Esta traducción se lleva a cabo mediante un servidor del sistema de nombres de dominio (DNS): una computadora que mantiene una base de datos de nombres de host y sus correspondientes direcciones IP; a este proceso se le conoce como búsqueda DNS (DNS lookup). El resto del URL (es decir, /libros/descargas.html) especifica tanto el nombre del recurso solicitado (el documento XHTML llamado descargas.html) como su ruta o ubicación (/libros), en el servidor Web. La ruta podría especificar la ubicación de un directorio actual en el sistema de archivos del servidor Web. Sin embargo, por cuestiones de seguridad, la ruta generalmente especifica la ubicación de un directorio virtual. El servidor traduce el directorio virtual en una ubicación real en el servidor (o en otra computadora en la red del servidor), con lo cual se oculta la verdadera ubicación del recurso. Algunos recursos se crean en forma dinámica, por lo que no residen en ninguna parte del servidor. El nombre de host en el URL para dicho recurso especifica el servidor correcto; la ruta y la información sobre el recurso identifican la ubicación del recurso con el que se va a responder a la petición del cliente. Cuando el navegador Web recibe un URL, realiza una transacción HTTP simple para obtener y mostrar la página Web que se encuentra en esa dirección. En la figura 26.1 se ilustra la transacción en forma detallada, mostrando la interacción entre el navegador Web (el lado cliente) y la aplicación servidor Web (el lado servidor). En la figura 26.1, el navegador Web envía una petición HTTP al servidor. La petición (en su forma más simple) es GET /libros/descargas.html HTTP/1.1

www.elsolucionario.net

1104 Capítulo 26 Aplicaciones Web: parte 1 La palabra GET es un método HTTP, el cual indica que el cliente desea obtener un recurso del servidor. El resto de la petición proporciona el nombre de la ruta del recurso (un documento XHTML), junto con el nombre del protocolo y el número de versión (HTTP/1.1). Cualquier servidor que entienda HTTP (versión 1.1) puede traducir esta petición y responder en forma apropiada. En la figura 26.2 se muestran los resultados de una petición exitosa. Primero, el servidor responde enviando una línea de texto que indica la versión de HTTP, seguida de un código numérico y una frase que describe el estado de la transacción. Por ejemplo, HTTP/1.1 200 OK

indica que se tuvo éxito, mientras que HTTP/1.1 404 Not found

informa al cliente que el servidor Web no pudo localizar el recurso solicitado. En la página www.w3.org/Protocols/HTTP/HTRESP.html encontrará una lista completa de códigos numéricos que indican el estado de una transacción HTTP.

a) El cliente envía la petición GET Servidor Web

al servidor Web

b) Después de recibir la petición, el servidor Web utiliza la información en el URL para localizar el recurso Cliente Internet

Figura 26.1 | Interacción entre el cliente y el servidor Web. Paso 1: la petición GET.

Servidor Web El servidor responde a la petición con un mensaje apropiado y el contenido del recurso

Cliente Internet

Figura 26.2 | Interacción entre el cliente y el servidor Web. Paso 2: la respuesta HTTP. Después, el servidor envía uno o más encabezados HTTP, los cuales proporcionan información adicional sobre los datos que se van a enviar. En este caso, el servidor está enviando un documento de texto XHTML, por lo que el encabezado HTTP para este ejemplo sería: Content-type: text/html

La información que se proporciona en este encabezado especifica el tipo de Extensiones de correo Internet multipropósito (MIME) del contenido que el servidor va a transmitir al navegador. MIME es un estándar de

www.elsolucionario.net

26.3 Arquitectura de aplicaciones multinivel 1105 Internet que especifica formatos de datos, para que los programas puedan interpretar los datos en forma correcta. Por ejemplo, el tipo MIME text/plain indica que la información enviada es texto que puede mostrarse directamente, sin interpretar el contenido como marcado de XHTML. De manera similar, el tipo MIME image/jpeg indica que el contenido es una imagen JPEG. Cuando el navegador recibe este tipo MIME, trata de mostrar la imagen. El encabezado, o conjunto de encabezados, va seguido por una línea en blanco, la cual indica al cliente que el servidor terminó de enviar encabezados HTTP. Después, el servidor envía el contenido del documento XHTML solicitado (descargas.html). El servidor termina la conexión cuando se completa la transferencia del recurso. El navegador del lado cliente analiza el marcado de XHTML que recibe y despliega (o visualiza) los resultados.

26.3 Arquitectura de aplicaciones multinivel Las aplicaciones basadas en Web son aplicaciones multinivel (comúnmente conocidas como aplicaciones de n niveles), que dividen la funcionalidad en niveles separados (es decir, agrupaciones lógicas de funcionalidad). Aunque los niveles pueden localizarse en la misma computadora, por lo general, los niveles de las aplicaciones basadas en Web residen en computadoras separadas. En la figura 26.3 se presenta la estructura básica de una aplicación basada en Web de tres niveles. El nivel inferior (también conocido como Nivel de datos o nivel de información) mantiene los datos de la aplicación. Por lo general, este nivel almacena los datos en un sistema de administración de bases de datos relacionales (RDBMS). En el capítulo 25 hablamos sobre los sistemas RDBMS. Por ejemplo, una tienda podría tener una base de datos de información sobre el inventario, que contenga descripciones de productos, precios y cantidades en almacén. La misma base de datos podría también contener información sobre los clientes, como los nombres de usuarios, direcciones de facturación y números de tarjetas de crédito. Podría haber varias bases de datos residiendo en una o más computadoras, que en conjunto forman los datos de la aplicación.

Nivel superior conocido también como Nivel de interfaz de usuario o nivel cliente

Navegador

Nivel intermedio también conocido como nivel de lógica de negocios

XHTML

Nivel inferior también conocido como Nivel de datos o nivel de información

JDBC Servidor Web

Base de datos

Figura 26.3 | Arquitectura de tres niveles. El nivel intermedio implementa la lógica de negocios, de controlador y de presentación para controlar las interacciones entre los clientes de la aplicación y sus datos. El nivel intermedio actúa como intermediario entre los datos en el nivel de información y los clientes de la aplicación. La lógica de control del nivel intermedio procesa las peticiones de los clientes (como las peticiones para ver un catálogo de productos) y obtiene datos de la base de datos. Después, la lógica de presentación del nivel intermedio procesa los datos del nivel de información y presenta el contenido al cliente. Por lo general, las aplicaciones Web presentan datos a los clientes en forma de documentos XHTML. La lógica comercial en el nivel intermedio hace valer las reglas comerciales y asegura que los datos sean confiables antes de que la aplicación servidor actualice la base de datos, o presente los datos a los usuarios. Las reglas comerciales dictan la forma en que los clientes pueden o no acceder a los datos de la aplicación, y la forma en que ésta procesa los datos. Por ejemplo, una regla comercial en el nivel intermedio de una aplicación basada en Web para una tienda podría asegurar que todas las cantidades de los productos sean siempre positivas. La petición de un cliente de establecer una cantidad negativa en la base de datos de información de productos del nivel inferior sería rechazada por la lógica comercial del nivel intermedio.

www.elsolucionario.net

1106 Capítulo 26 Aplicaciones Web: parte 1 El nivel superior (nivel cliente) es la interfaz de usuario de la aplicación, la cual recopila los datos de entrada y de salida. Los usuarios interactúan en forma directa con la aplicación a través de la interfaz de usuario, que por lo general es el navegador Web, el teclado y el ratón. En respuesta a las acciones del usuario (por ejemplo, hacer clic en un hipervínculo), el nivel cliente interactúa con el nivel intermedio para hacer peticiones y obtener datos del nivel de información. Después, el nivel cliente muestra los datos obtenidos para el usuario. El nivel cliente nunca interactúa directamente con el nivel de información. Las aplicaciones multinivel de Java se implementan comúnmente mediante el uso de las características de Java Enterprise Edition (Java EE). Las tecnologías que usaremos para desarrollar aplicaciones Web en los capítulos 26 a 28 son parte de Java EE 5 (java.sun.com/javaee).

26.4 Tecnologías Web de Java Las tecnologías Web de Java evolucionan en forma continua, para ofrecer a los desarrolladores niveles mayores de abstracción, y una mayor separación de los niveles de la aplicación. Esta separación facilita el mantenimiento y la extensibilidad de las aplicaciones Web. Un diseñador gráfico puede crear la interfaz de usuario de la aplicación sin tener que preocuparse por la lógica de páginas subyacente, la cual estará a cargo de un programador. Mientras tanto, el programador está libre para enfocarse en la lógica comercial de la aplicación, dejando al diseñador los detalles sobre la construcción de una aplicación atractiva y fácil de usar. Java Studio Creator 2 es el paso más reciente en esta evolución, ya que nos permite desarrollar la GUI de una aplicación Web mediante una herramienta de diseño tipo “arrastrar y soltar”, mientras que podemos manejar la lógica comercial en clases de Java separadas.

26.4.1 Servlets Los servlets son la vista de nivel más bajo de las tecnologías de desarrollo en Java que veremos en este capítulo. Utilizan el modelo petición-respuesta HTTP de comunicación entre cliente y servidor. Los servlets extienden la funcionalidad de un servidor, al permitir que éste genere contenido dinámico. Por ejemplo, los servlets pueden generar en forma dinámica documentos XHTML personalizados, ayudar a proporcionar un acceso seguro a un sitio Web, interactuar con bases de datos a beneficio de un cliente y mantener la información de sesión única para cada cliente. Un componente del servidor Web, conocido como contenedor de servlets, ejecuta los servlets e interactúa con ellos. Los paquetes javax.servlet y javax.servlet.http proporcionan las clases e interfaces para definir servlets. El contenedor de servlets recibe peticiones HTTP de un cliente y dirige cada petición al servlet apropiado. El servlet procesa la petición y devuelve una respuesta apropiada al cliente; por lo general en forma de un documento XHTML o XML (Lenguaje de marcado extensible) para mostrarlo en el navegador. XML es un lenguaje que se utiliza para intercambiar datos estructurados en la Web. Desde el punto de vista arquitectónico, todos los servlets deben implementar a la interfaz Servlet del paquete javax.servlet, la cual asegura que cada servlet se pueda ejecutar en el marco de trabajo proporcionado por el contenedor de servlets. La interfaz Servlet declara métodos que el contenedor de servlets utiliza para administrar el ciclo de vida del servlet. Este ciclo de vida empieza cuando el contenedor de servlets lo carga en memoria; por lo general, en respuesta a la primera petición del servlet. Antes de que el servlet pueda manejar esa petición, el contenedor invoca al método init del servlet, el cual se llama sólo una vez durante el ciclo de vida de un servlet para inicializarlo. Una vez que init termina su ejecución, el servlet está listo para responder a su primera petición. Todas las peticiones se manejan mediante el método service de un servlet, el cual es el método clave para definir la funcionalidad de un servlet. El método service recibe la petición, la procesa y envía una respuesta al cliente. Durante el ciclo de vida de un servlet, se hace una llamada al método service por cada petición. Cada nueva petición se maneja comúnmente en un subproceso de ejecución separado (administrado por el contenedor de servlets), por lo que cada servlet debe ser seguro para los subprocesos. Cuando el contenedor de servlets termina el servlet (por ejemplo, cuando el contenedor de servlets necesita más memoria o cuando se cierra), se hace una llamada al método destroy del servlet para liberar los recursos que éste ocupa.

26.4.2 JavaServer Pages La tecnología JavaServer Pages (JSP) es una extensión de la tecnología de los servlets. El contenedor de JSPs traduce cada JSP y la convierte en un servlet. A diferencia de los servlets, las JSPs nos ayudan a separar la presentación del contenido. Las JavaServer Pages permiten a los programadores de aplicaciones Web crear contenido dinámico mediante la reutilización de componentes predefinidos, y mediante la interacción con componentes que utilizan secuencias de comandos del lado servidor. Los programadores de JSPs pueden utilizar componen-

www.elsolucionario.net

26.4 Tecnologías Web de Java 1107 tes especiales de software llamados JavaBeans, y bibliotecas de etiquetas personalizadas que encapsulan una funcionalidad dinámica y compleja. Un JavaBean es un componente reutilizable que sigue ciertas convenciones para el diseño de clases. Por ejemplo, las clases de JavaBeans que permiten operaciones de lectura y escritura de variables de instancias deben proporcionar métodos obtener (get) y establecer (set) apropiados. El conjunto completo de convenciones de diseño de clases se describe en la especificación de los JavaBeans (java.sun.com/products/ javabeans/glasgow/index.html).

Bibliotecas de etiquetas personalizadas Las bibliotecas de etiquetas personalizadas son una poderosa característica de la tecnología JSP, que permite a los desarrolladores de Java ocultar el código para acceder a una base de datos y otras operaciones complejas mediante etiquetas personalizadas. Para usar dichas herramientas, sólo tenemos que agregar las etiquetas personalizadas a la página. Esta simpleza permite a los diseñadores de páginas Web, que no estén familiarizados con Java, mejorar las páginas Web con poderoso contenido dinámico y capacidades de procesamiento. Las clases e interfaces de JSP se encuentran en los paquetes javax.servlet.jsp y javax.servlet.jsp.tagext.

Componentes de JSP Hay cuatro componentes clave para las JSPs: directivas, acciones, elementos de secuencia de comandos y bibliotecas de etiquetas. Las directivas son mensajes para el contenedor de JSPs: el componente del servidor Web que ejecuta las JSPs. Las directivas nos permiten especificar configuraciones de páginas, para incluir contenido de otros recursos y especificar bibliotecas de etiquetas personalizadas para usarlas en las JSPs. Las acciones encapsulan la funcionalidad en etiquetas predefinidas que los programadores pueden incrustar en JSPs. A menudo, las acciones se realizan con base en la información que se envía al servidor como parte de una petición específica de un cliente. También pueden crear objetos de Java para usarlos en las JSPs. Los elementos de secuencia de comandos permiten al programador insertar código que interactúe con los componentes en una JSP (y posiblemente con otros componentes de aplicaciones Web) para realizar el procesamiento de peticiones. Las bibliotecas de etiquetas forman parte del mecanismo de extensión de etiquetas que permite a los programadores crear etiquetas personalizadas. Dichas etiquetas permiten a los diseñadores de páginas Web manipular el contenido de las JSPs sin necesidad de tener un conocimiento previo sobre Java. La Biblioteca de etiquetas estándar de JavaServer Pages (JSTL) proporciona la funcionalidad para muchas tareas de aplicaciones Web comunes, como iterar a través de una colección de objetos y ejecutar instrucciones de SQL.

Contenido estático Las JSPs pueden contener otro tipo de contenido estático. Por ejemplo, las JSPs comúnmente incluyen marcado XHTML o XML. A dicho marcado se le conoce como datos de plantilla fija o texto de plantilla fija. Cualquier texto literal en una JSP se traduce en una literal String en la representación de la JSP en forma de servlet.

Procesamiento de una petición de JSP Cuando un servidor habilitado para JSP recibe la primera petición para una JSP, el contenedor de JSPs traduce esa JSP en un servlet, el cual maneja la petición actual y las futuras peticiones a esa JSP. Por lo tanto, las JSPs se basan en el mismo mecanismo de petición-respuesta que los servlets para procesar las peticiones de los clientes, y enviar las respuestas.

Tip de rendimiento 26.1 Algunos contenedores de JSPs traducen las JSPs en servlets al momento de desplegar las JSPs (es decir, cuando la aplicación se coloca en un servidor Web). Esto elimina la sobrecarga de la traducción para el primer cliente que solicita cada JSP, ya que la JSP se traducirá antes de que un cliente la haya solicitado.

26.4.3 JavaServer Faces JavaServer Faces (JSF) es un marco de trabajo para aplicaciones Web que simplifica el diseño de la interfaz de usuario de una aplicación, y separa aún más la presentación de una aplicación Web de su lógica comercial. Un marco de trabajo (framework) simplifica el desarrollo de aplicaciones, al proporcionar bibliotecas y (algunas veces) herramientas de software para ayudar al programador a organizar y crear sus aplicaciones. Aunque el marco

www.elsolucionario.net

1108 Capítulo 26 Aplicaciones Web: parte 1 de trabajo JSF puede usar muchas tecnologías para definir las páginas en las aplicaciones Web, este capítulo se enfoca en las aplicaciones JSF que utilizan JavaServer Pages. JSF proporciona un conjunto de componentes de interfaz de usuario, o componentes de JSF que simplifican el diseño de páginas Web. Estos componentes son similares a los componentes de Swing que se utilizan para crear aplicaciones con GUI. JSF proporciona dos bibliotecas de etiquetas personalizadas de JSP para agregar estos componentes a una página JSP. JSF también incluye APIs para manejar eventos de componentes (como el procesamiento de los cambios de estado de los componentes y la validación de la entrada del usuario), navegar entre las páginas de una aplicación Web y mucho más. El programador diseña la apariencia visual de una página con JSF, agregando etiquetas a un archivo JSP y manipulando sus atributos. El programador define el comportamiento de la página por separado, en un archivo de código fuente de Java relacionado. Aunque los componentes estándar de JSF son suficientes para la mayoría de las aplicaciones Web básicas, también podemos escribir bibliotecas de componentes personalizados. Hay bibliotecas de componentes adicionales, disponibles en el proyecto Java BluePrints, el cual muestra las mejores prácticas para desarrollar aplicaciones en Java. Muchos otros distribuidores ofrecen bibliotecas de componentes de JSF. Por ejemplo, Oracle proporciona alrededor de 100 componentes en su biblioteca ADF Faces. Aquí hablaremos sobre una de esas bibliotecas de componentes, conocida como BluePrints AJAX (blueprints.dev.java.net/ajaxcomponents.html). En el siguiente capítulo hablaremos sobre los componentes de Java BluePrints para crear aplicaciones de JSF habilitadas para AJAX.

26.4.4 Tecnologías Web en Java Studio Creator 2 Las aplicaciones Web de Java Studio Creator 2 consisten en una o más páginas Web JSP, integradas en el marco de trabajo JavaServer Faces. Estos archivos JSP tienen la extensión de archivo .jsp y contienen los elementos de la GUI de la página Web. Las JSPs también pueden contener JavaScript para agregar funcionalidad a la página. Las JSPs se pueden personalizar en Java Studio Creator 2 al agregar componentes de JSF, incluyendo etiquetas, campos de texto, imágenes, botones y otros componentes de GUI. El IDE nos permite diseñar las páginas en forma visual, al arrastrar y soltar estos componentes en una página; también podemos personalizar una página Web al editar el archivo .jsp en forma manual. Cada archivo JSP que se crea en Java Studio Creator 2 representa una página Web, y tiene su correspondiente clase JavaBean, denominada bean de página. Una clase JavaBean debe tener un constructor predeterminado (o sin argumentos), junto con métodos obtener (get) y establecer (set) para todas las propiedades del bean (es decir, las variables de instancia). El bean de página define las propiedades para cada uno de los elementos de la página. El bean de página también contiene los manejadores de eventos y los métodos de ciclo de vida de las páginas para administrar tareas, como la inicialización y despliegue de las páginas, y demás código de soporte para la aplicación Web. Toda aplicación Web creada con Java Studio Creator 2 tiene otros tres JavaBeans. El objeto RequestBean se mantiene en ámbito de petición; este objeto existe sólo mientras dure una petición HTTP. Un objeto SessionBean tiene ámbito de sesión; el objeto existe durante una sesión de navegación del usuario, o hasta que se agota el tiempo de la sesión. Hay un único objeto SessionBean para cada usuario. Por último, el objeto ApplicationBean tiene ámbito de aplicación; este objeto es compartido por todas las instancias de una aplicación y existe mientras que la aplicación esté desplegada en un servidor Web. Este objeto se utiliza para almacenar datos a nivel de aplicación o para procesamiento; sólo existe una instancia para la aplicación, sin importar el número de sesiones abiertas.

26.5 Creación y ejecución de una aplicación simple en Java Studio Creator 2 Nuestro primer ejemplo muestra la hora del día del servidor Web en una ventana del navegador. Al ejecutarse, este programa muestra el texto "Hora actual en el servidor Web", seguido de la hora del servidor Web. La aplicación contiene una sola página Web y, como mencionamos antes, consiste de dos archivos relacionados: un archivo JSP (figura 26.4) y un archivo de bean de página de soporte (figura 26.6). La aplicación tiene también los tres beans de datos con ámbito para los ámbitos de petición, sesión y aplicación. Como esta aplicación no almacena datos, estos beans no se utilizan en este ejemplo. Primero hablaremos sobre el marcado en el archivo JSP, el código en el archivo de bean de página y la salida de la aplicación; después proporcionaremos instrucciones

www.elsolucionario.net

26.5 Creación y ejecución de una aplicación simple en Java Studio Creator 2 1109 detalladas para crear el programa. [Nota: el marcado en la figura 26.4 y en los demás listados de archivos JSP en este capítulo es el mismo que el marcado que aparece en Java Studio Creator 2, pero hemos cambiado el formato de estos listados para fines de presentación, para que el código sea más legible]. Java Studio Creator 2 genera todo el marcado que se muestra en la figura 26.4 cuando establecemos el título de la página Web, arrastramos dos componentes Texto estático en la página y establecemos las propiedades de estos componentes. Los componentes Texto estático muestran texto que el usuario no puede editar. En breve le mostraremos estos pasos.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38



Figura 26.4 | Archivo JSP generado por Java Studio Creator 2, que muestra la hora actual en el servidor Web.

26.5.1 Análisis de un archivo JSP Los archivos JSP que se utilizan en este ejemplo (y los siguientes) se generan casi completamente mediante Java Studio Creator 2, el cual proporciona un Editor visual que nos permite crear la GUI de una página al arrastrar y soltar componentes en un área de diseño. El IDE genera un archivo JSP en respuesta a las interacciones del programador. En la línea 1 de la figura 26.4 está la declaración XML, la cual indica que la JSP está expresada en sintaxis XML, junto con la versión de XML que se utiliza. En las líneas 3 a 5 hay comentarios que agregamos a la JSP, para indicar su número de figura, nombre de archivo y propósito.

www.elsolucionario.net

1110 Capítulo 26 Aplicaciones Web: parte 1 En la línea 6 empieza el elemento raíz para la JSP. Todas las JSPs deben tener este elemento jsp:root, el cual tiene un atributo version para indicar la versión de JSP que se está utilizando (línea 6), y uno o más atributos xmlns (líneas 7 a 10). Cada atributo xmlns especifica un prefijo y un URL para una biblioteca de etiquetas, lo cual permite a la página usar las etiquetas especificadas en esa biblioteca. Por ejemplo, la línea 9 permite a la página usar los elementos estándar de las JSPs. Para usar estos elementos, hay que colocar el prefijo jsp antes de la etiqueta de cada elemento. Todas las JSPs generadas por Java Studio Creator 2 incluyen las bibliotecas de etiquetas especificadas en las líneas 7 a 10 (la biblioteca de componentes JSF básicos, la biblioteca de componentes JSF de HTML, la biblioteca de componentes JSP estándar y la biblioteca de componentes JSF de interfaz de usuario). En las líneas 11 y 12 se encuentra el elemento jsp:directive.page. Su atributo contentType especifica el tipo MIME (text/html) y el conjunto de caracteres (UTF-8) que utiliza la página. El atributo pageEncoding especifica la codificación de caracteres que utiliza el origen de la página. Estos atributos ayudan al cliente (por lo general, un navegador Web) a determinar cómo desplegar el contenido. Todas las páginas que contienen componentes JSF se representan en un árbol de componentes (figura 26.5) con el elemento JSF raíz f:view, que es de tipo UIViewRoot. Para representar la estructura de este árbol de componentes en una JSP, se encierran todas las etiquetas de los componentes JSF dentro del elemento f:view (líneas 13 a 37).

jsp:root

jsp:directive

f:view

ui:html

ui:body

ui:head

ui:link

(proporciona la hoja de estilo)

(los hijos son componentes visibles)

ui:StaticText

ui:Button

Figura 26.5 | Árbol de componentes JSF de ejemplo. En las líneas 14 a 20 empieza la definición de la JSP con las etiquetas ui:page, ui:html y ui:head, todas de la biblioteca de etiquetas ui (componentes JSF de interfaz de usuario). Éstos y muchos otros elementos de página tienen un atributo binding. Por ejemplo, el elemento ui:head (línea 16) tiene el atributo binding = "#{Hora. head}.". Este atributo utiliza la notación del Lenguaje de expresiones JSF (es decir, #{Hora.head}) para hacer referencia a la propiedad head en la clase Hora que representa al bean de página (en la figura 26.6 podrá ver esta clase). Es posible enlazar un solo atributo de un elemento JSP a una propiedad en cualquiera de los JavaBeans de la aplicación Web. Por ejemplo, el atributo text de un componente ui:label se puede enlazar a una propiedad String en el objeto SessionBean de la aplicación. En la sección 26.7.2 veremos un ejemplo de esto. El elemento ui:head (líneas 16 a 20) tiene un atributo title que especifica el título de la página. Este elemento también contiene un elemento ui:link (líneas 18 y 19), el cual especifica la hoja de estilo CSS que utiliza la página. El elemento ui:body (líneas 22 a 34) contiene un elemento ui:form (líneas 24 a 33), el cual contiene

www.elsolucionario.net

26.5 Creación y ejecución de una aplicación simple en Java Studio Creator 2 1111 dos componentes ui:staticText (líneas 25 a 28 y 29 a 32). Estos componentes muestran el texto de la página. El componente encabezadoHora (líneas 25 a 28) tiene un atributo text (líneas 27 y 28) que especifica el texto a mostrar (es decir, "Hora actual en el servidor Web:"). El componente textoReloj (líneas 29 a 32) no especifica un atributo de texto, ya que el texto de este componente se establecerá mediante programación. Para que el marcado en este archivo se muestre en un navegador Web, todos los elementos de la JSP se asignan automáticamente a elementos de XHTML que el navegador reconoce. El mismo componente Web se puede asignar a varios elementos de XHTML distintos, dependiendo del navegador Web cliente y de las configuraciones de las propiedades del componente. En este ejemplo, los componentes ui:staticText (líneas 25 a 28, 29 a 32) se asignan a elementos span de XHTML. Un elemento span contiene texto que se muestra en una página Web, y que comúnmente se utiliza para controlar el formato del texto. Los atributos style de un elemento ui:staticText de una JSP se representan como parte del correspondiente atributo style del elemento span cuando el navegador despliega la página. En un momento le mostraremos el documento XHTML que se produce cuando un navegador solicita la página Hora.jsp.

26.5.2 Análisis de un archivo de bean de página En la figura 26.6 se presenta el archivo de bean de página. En la línea 3 se indica que esta clase pertenece al paquete horaweb. Esta línea se genera automáticamente y especifica el nombre del proyecto como el nombre del paquete. En la línea 17 empieza la declaración de la clase Hora e indica que hereda de la clase AbstractPageBean (del paquete com.sun.rave.web.ui.appbase). Todas las clases de bean de página que soportan archivos JSP con componentes JSF deben heredar de la clase abstracta AbstractPageBean, la cual proporciona métodos para el ciclo de vida de las páginas. Observe que el IDE hace que el nombre de la clase coincida con el nombre de la página. El paquete com.sun.rave.web.ui.component incluye clases para muchos de los componentes JSF básicos (vea las instrucciones import en las líneas 6 a 11 y 13).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

// Fig. 26.6: Hora.java // Archivo de bean de página que establece textoReloj a la hora en el servidor Web. package horaweb; import import import import import import import import import import import

com.sun.rave.web.ui.appbase.AbstractPageBean; com.sun.rave.web.ui.component.Body; com.sun.rave.web.ui.component.Form; com.sun.rave.web.ui.component.Head; com.sun.rave.web.ui.component.Html; com.sun.rave.web.ui.component.Link; com.sun.rave.web.ui.component.Page; javax.faces.FacesException; com.sun.rave.web.ui.component.StaticText; java.text.DateFormat; java.util.Date;

public class Hora extends AbstractPageBean { private int __placeholder; // método de inicialización de componentes, generado automáticamente. private void _init() throws Exception { // cuerpo vacío } // fin del método _init private Page page1 = new Page(); public Page getPage1() {

Figura 26.6 | Archivo de bean de página que establece textoReloj a la hora en el servidor Web. (Parte 1 de 4).

www.elsolucionario.net

1112 Capítulo 26 Aplicaciones Web: parte 1

31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89

return page1; } // fin del método getPage1 public void setPage1(Page p) { this.page1 = p; } // fin del método setPage1 private Html html1 = new Html(); public Html getHtml1() { return html1; } // fin del método getHtml1 public void setHtml1(Html h) { this.html1 = h; } // fin del método setHtml1 private Head head1 = new Head(); public Head getHead1() { return head1; } // fin del método getHead1 public void setHead1(Head h) { this.head1 = h; } // fin del método setHead1 private Link link1 = new Link(); public Link getLink1() { return link1; } // fin del método getLink1 public void setLink1(Link l) { this.link1 = l; } // fin del método setLink1 private Body body1 = new Body(); public Body getBody1() { return body1; } // fin del método getBody1 public void setBody1(Body b) { this.body1 = b; } // fin del método setBody1 private Form form1 = new Form(); public Form getForm1()

Figura 26.6 | Archivo de bean de página que establece textoReloj a la hora en el servidor Web. (Parte 2 de 4).

www.elsolucionario.net

26.5 Creación y ejecución de una aplicación simple en Java Studio Creator 2 1113

90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148

{ return form1; } // fin del método getForm1 public void setForm1(Form f) { this.form1 = f; } // fin del método setForm1 private StaticText encabezadoHora = new StaticText(); public StaticText getEncabezadoHora() { return encabezadoHora; } // fin del método getEncabezadoHora public void setEncabezadoHora(StaticText st) { this.encabezadoHora = st; } // fin del método setEncabezadoHora private StaticText textoReloj = new StaticText(); public StaticText getTextoReloj() { return textoReloj; } // fin del método getTextoReloj public void setTextoReloj(StaticText st) { this.textoReloj = st; } // fin del método setTextoReloj // Construye una nueva instancia de bean de página public Hora() { // constructor vacío } // fin del constructor // Devuelve una referencia al bean de datos con ámbito protected RequestBean1 getRequestBean1() { return (RequestBean1)getBean("RequestBean1"); } // fin del método getRequestBean1 // Devuelve una referencia al bean de datos con ámbito protected ApplicationBean1 getApplicationBean1() { return (ApplicationBean1)getBean("ApplicationBean1"); } // fin del método getApplicationBean1 // Devuelve una referencia al bean de datos con ámbito protected SessionBean1 getSessionBean1() { return (SessionBean1)getBean("SessionBean1"); } // fin del método getSessionBean1 // inicializa el contenido de la página public void init()

Figura 26.6 | Archivo de bean de página que establece textoReloj a la hora en el servidor Web. (Parte 3 de 4).

www.elsolucionario.net

1114 Capítulo 26 Aplicaciones Web: parte 1

149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181

{ super.init(); try { _init(); } // fin de try catch ( Exception e ) { log( "Error al inicializar Hora", e ); throw e instanceof FacesException ? ( FacesException ) e: new FacesException( e ); } // fin de catch } // fin del método init // método que se llama cuando ocurre una petición de devolución de envío public void preprocess() { // cuerpo vacío } // fin del método preprocess // método al que se llama antes de desplegar la página public void prerender() { textoReloj.setValue( DateFormat.getTimeInstance( DateFormat.LONG ).format( new Date() ) ); } // fin del método prerender // método al que se llama una vez que se completa el despliegue, si se llamó a init public void destroy() { // cuerpo vacío } // fin del método destroy } // fin de la clase Hora

Figura 26.6 | Archivo de bean de página que establece textoReloj a la hora en el servidor Web. (Parte 4 de 4).

Este archivo de bean de página proporciona métodos obtener (get) y establecer (set) para cada elemento del archivo JSP de la figura 26.4. El IDE genera estos métodos de manera automática. Incluimos el archivo de bean de página completo en este primer ejemplo, pero en los siguientes ejemplos omitiremos estas propiedades y sus métodos obtener y establecer para ahorrar espacio. En las líneas 99 a 109 y 111 a 121 del archivo de bean de página se definen los dos componentes Static Text que soltamos en la página, junto con sus métodos obtener y establecer. Estos componentes son objetos de la clase StaticText en el paquete com.sun.rave.web.ui.component. La única lógica requerida en esta página es establecer el texto del componente textoReloj para que lea la hora actual en el servidor. Esto lo hacemos en el método prerender (líneas 170 a 174). Más adelante hablaremos sobre el significado de éste y otros métodos de bean de página. En las líneas 172 y 173 se obtiene y da formato a la hora en el servidor, y se establece el valor de textoReloj con esa hora.

www.elsolucionario.net

26.5 Creación y ejecución de una aplicación simple en Java Studio Creator 2 1115

26.5.3 Ciclo de vida del procesamiento de eventos El modelo de aplicación de Java Studio Creator 2 coloca varios métodos en el bean de página, los cuales se enlazan en el ciclo de vida del procesamiento de eventos. Estos métodos representan cuatro etapas principales: inicialización, pre-procesamiento, pre-despliegue y destrucción. Cada uno de ellos corresponde a un método en la clase de bean de página: init, preprocess, prerender y destroy, respectivamente. Java Studio Creator 2 crea estos métodos de manera automática, pero podemos personalizarlos para manejar las tareas de procesamiento del ciclo de vida, como desplegar un elemento en una página sólo si un usuario hace clic en un botón. El método init (figura 26.6, líneas 148 a 161) es llamado por el contenedor de JSPs la primera vez que se solicita la página, y en las peticiones de devolución de envío. Una petición de devolución de envío (postback) ocurre cuando se envían los datos de un formulario, y la página junto con su contenido se envían al servidor para ser procesados. El método init invoca la versión de su superclase (línea 150) y después trata de llamar al método _init (declarado en las líneas 22 a 25). El método _init también se genera en forma automática, y maneja las tareas de inicialización de componentes (si los hay), como establecer las opciones para un grupo de botones de opción. El método preprocess (líneas 164 a 167) se llama después de init, pero sólo si la página está procesando una petición de devolución de envío. El método prerender (líneas 170 a 174) se llama justo antes de que el navegador despliegue (muestre) una página. Este método se debe utilizar para establecer las propiedades de los componentes; las propiedades que se establecen antes (como en el método init) pueden sobrescribirse antes de que el navegador despliegue la página. Por esta razón, establecemos el valor de textoReloj en el método prerender. Por último, el método destroy (líneas 177 a 180) se llama una vez que la página se ha desplegado, pero sólo si se hizo la llamada al método init. Este método maneja tareas tales como liberar los recursos que se utilizan para desplegar la página.

26.5.4 Relación entre la JSP y los archivos de bean de página El bean de página tiene una propiedad para cada elemento que aparece en el archivo JSP de la figura 26.4, desde el elemento html hasta los dos componentes Texto estático. Recuerde que los elementos en el archivo JSP se enlazaron explícitamente a estas propiedades mediante el atributo binding de cada elemento, usando una instrucción en Lenguaje de expresiones JSF. Como ésta es una clase JavaBean, también se incluyen métodos obtener (get) y establecer (set) para cada una de estas propiedades (líneas 27 a 121). El IDE genera este código automáticamente para cada proyecto de aplicación Web.

26.5.5 Análisis del XHTML generado por una aplicación Web de Java En la figura 26.7 se muestra el XHTML que se genera cuando un navegador Web cliente solicita la página Hora. jsp (figura 26.4). Para ver este XHTML, seleccione Ver > Código fuente en Internet Explorer. [Nota: agregamos los comentarios de XHTML en las líneas 3 y 4, y cambiamos el formato del XHTML para que se conforme a nuestras convenciones de codificación]. El documento XHTML en la figura 26.7 es similar en estructura al archivo JSP de la figura 26.4. En las líneas 5 y 6 está la declaración del tipo de documento, la cual lo declara como documento XHTML 1.0 Transicional. Las etiquetas ui:meta en las líneas 9 a 13 son equivalentes a los encabezados HTTP, y se utilizan para controlar el comportamiento del navegador Web.

1 2 3 4 5 6 7 8 9



Figura 26.7 | Respuesta XHTML generada cuando el navegador solicita el archivo Hora.jsp. (Parte 1 de 2).

www.elsolucionario.net

1116 Capítulo 26 Aplicaciones Web: parte 1

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

Hora Web: un ejemplo simple var sjwuic_ScrollCookie = new sjwuic_ScrollCookie( '/Hora.jsp', '/HoraWeb/faces/Hora.jsp' ); Hora actual en el servidor Web: 10:28:47 PM CDT

Figura 26.7 | Respuesta XHTML generada cuando el navegador solicita el archivo Hora.jsp. (Parte 2 de 2).

En las líneas 30 a 43 se define el cuerpo (body) del documento. En la línea 31 empieza el formulario (form), un mecanismo para recolectar información del usuario y enviarla de vuelta al servidor Web. En este programa específico, el usuario no envía datos al servidor Web para procesarlos; sin embargo, el procesamiento de los datos del usuario es una parte imprescindible de muchas aplicaciones Web, la cual se facilita mediante el uso de los formularios. En ejemplos posteriores demostraremos cómo enviar datos al servidor. Los formularios XHTML pueden contener componentes visuales y no visuales. Los componentes visuales incluyen botones y demás componentes de GUI con los que interactúan los usuarios. Los componentes no visuales, llamados elementos de formulario hidden, almacenan datos tales como direcciones de e-mail, que el autor del documento especifica. Una de estas entradas ocultas se define en las líneas 40 y 41. Más adelante en este capítulo hablaremos sobre el significado preciso de esta entrada oculta. El atributo method del elemento form (línea 31) especifica el método mediante el cual el navegador Web envía el formulario al servidor. De manera predeterminada, las JSPs utilizan el método post. Los dos tipos de peticiones HTTP más comunes (también conocidas como métodos de petición) son get y post. Una petición get obtiene (o recupera) la información de un servidor. Dichas peticiones comúnmente recuperan un documento HTML o una imagen. Una petición post envía datos a un servidor, como la información de autenticación o los datos de un formulario que recopilan la entrada del usuario. Por lo general, las peticiones post se utilizan para enviar un mensaje a un grupo de noticias o a un foro de discusión, pasar la entrada del usuario a un proceso manejador de datos en el servidor, y almacenar o actualizar los datos en un servidor. El atributo action de form (línea 32) identifica el recurso que se pedirá cuando se envíe este formulario; en este caso, /HoraWeb/faces/Hora.jsp.

www.elsolucionario.net

26.5 Creación y ejecución de una aplicación simple en Java Studio Creator 2 1117 Observe que los dos componentes Texto estático (es decir, encabezadoHora y textoReloj) se representan mediante dos elementos span en el documento XHTML (líneas 34 a 36, 37 a 39) como vimos anteriormente. Las opciones de formato que se especificaron como propiedades de encabezadoHora y textoReloj, como el tamaño de la fuente y el color del texto en los componentes, ahora se especifican en el atributo style de cada elemento span.

26.5.6 Creación de una aplicación Web en Java Studio Creator 2 Ahora que hemos presentado el archivo JSP, el archivo de bean de página y la página Web de XHTML resultante que se envía al navegador Web, vamos a describir los pasos para crear esta aplicación. Para crear la aplicación HoraWeb, realice los siguientes pasos en Java Studio Creator 2:

Paso 1: Creación del proyecto de aplicación Web Seleccione Archivo > Nuevo proyecto… para mostrar el cuadro de diálogo Nuevo proyecto. En este cuadro de diálogo, seleccione Web en el panel Categorías, Aplicación Web JSF en el panel Proyectos y haga clic en Siguiente. Cambie el nombre del proyecto a HoraWeb y use la ubicación predeterminada del proyecto y el paquete Java predeterminado. Estas opciones crearán un directorio HoraWeb en su directorio Mis documentos\Creator\Projects para almacenar los archivos del proyecto. Haga clic en Terminar para crear el proyecto de aplicación Web.

Paso 2: Análisis de la ventana del Editor visual del nuevo proyecto Las siguientes figuras describen características importantes del IDE, empezando con la ventana Editor visual (figura 26.8). Java Studio Creator 2 crea una sola página Web llamada Page1 cuando se crea un nuevo proyecto. Esta página se abre de manera predeterminada en el Editor visual en modo Diseño, cuando el proyecto se carga por primera vez. A medida que arrastre y suelte nuevos componentes en la página, el modo Diseño le permitirá ver cómo se desplegará su página en el navegador. El archivo JSP para esta página, llamado Page1.jsp, se puede ver haciendo clic en el botón JSP que se encuentra en la parte superior del Editor visual, o haciendo clic con el botón derecho del ratón en cualquier parte dentro del Editor visual y seleccionando la opción Editar origen JSP. Como dijimos antes, cada página Web está soportada por un archivo de bean de página. Java Studio Creator 2 crea un archivo llamado Page1.java cuando se crea un nuevo proyecto. Para abrir este archivo, haga clic en el botón Java que se encuentra en la parte superior del Editor visual, o haga clic con el botón derecho del ratón en cualquier parte dentro del Editor visual y seleccione la opción Editar origen Java Page1.

Previsualizar en navegador

Actualizar

Mostrar formularios virtuales

Modo Diseño Vista del archivo JSP Vista del archivo Java de bean de página

Figura 26.8 | Ventana Editor visual en modo Diseño.

www.elsolucionario.net

Tamaño de navegador de destino

1118 Capítulo 26 Aplicaciones Web: parte 1 El botón Previsualizar en navegador en la parte superior de la ventana Editor visual le permite ver sus páginas en un navegador sin tener que crear y ejecutar la aplicación. El botón Actualizar vuelve a dibujar la página en el Editor visual. El botón Mostrar formularios virtuales nos permite ver qué elementos de formulario están participando en los formularios virtuales (hablaremos sobre este concepto en el capítulo 27). La lista desplegable Tamaño de navegador de destino nos permite especificar la resolución óptima del navegador para ver la página, y nos permite ver cuál será la apariencia de la página en distintas resoluciones de pantalla.

Paso 3: Análisis de la Paleta en Java Studio Creator 2 En la figura 26.9 se muestra la Paleta que aparece en el IDE cuando se carga el proyecto. La parte a) muestra el inicio de la lista Básicos de componentes Web, y la parte b) muestra el resto de los componentes Básicos, junto con la lista de componentes Diseño. Hablaremos sobre componentes específicos de la figura 26.9 a medida que los utilicemos en el capítulo.

Paso 4: Análisis de la ventana Proyectos En la figura 26.10 se muestra la ventana Proyectos, la cual aparece en la esquina inferior derecha del IDE. Esta ventana muestra la jerarquía de todos los archivos incluidos en el proyecto. Los archivos JSP para cada página se enlistan en el nodo Páginas Web. Este nodo también incluye la carpeta resources, la cual contiene la hoja de estilo CSS para el proyecto, y cualquier otro archivo que puedan necesitar las páginas para mostrarse en forma apropiada, como los archivos de imagen. Todo el código fuente de Java, incluyendo el archivo de bean de página para cada página Web y los beans de aplicación, sesión y petición, se pueden encontrar bajo el nodo Paquetes de origen. Otro archivo útil que se muestra en la ventana del proyecto es el archivo Navegación de página, el cual define las reglas para navegar por las páginas del proyecto, con base en el resultado de algún evento iniciado por el usuario, como hacer clic en un botón o en un vínculo. También podemos acceder al archivo Navegación de página si hacemos clic con el botón derecho del ratón en el Editor visual, estando en modo Diseño; para ello, seleccionamos la opción Navegación de página….

Paso 5: Análisis de los archivos JSP y Java en el IDE En la figura 26.11 se muestra Page1.jsp; el archivo JSP generado por Java Studio Creator 2 para Page1. [Nota: cambiamos el formato del código para adaptarlo a nuestras convenciones de codificación]. Haga clic en el botón JSP que está en la parte superior del Editor visual para abrir el archivo JSP. Cuando se crea por primera vez, este

a)

b)

Figura 26.9 | La Paleta en Java Studio Creator 2.

www.elsolucionario.net

26.5 Creación y ejecución de una aplicación simple en Java Studio Creator 2 1119

Hoja de estilo CSS, imágenes y demás recursos de página Archivo JSP Archivo de definición Navegación de página

Archivo Java de bean de página

Figura 26.10 | Ventana Proyectos para el proyecto HoraWeb.

Figura 26.11 | Archivo JSP generado para la página 1 (Page1) por Java Studio Creator 2. archivo contiene varias etiquetas para configurar la página, incluyendo creación de vínculos a la hoja de estilo de la página y definir las bibliotecas JSF necesarias. En cualquier otro caso, las etiquetas del archivo JSP están vacías, ya que no se han agregado todavía componentes a la página. En la figura 26.12 se muestra parte de Page1.java; el archivo de bean de página generado por Java Studio Creator 2 para Page1. Haga clic en el botón Java que está en la parte superior del Editor visual para abrir el archivo de bean de página. Este archivo contiene una clase de Java con el mismo nombre que la página (es decir, Page1), la cual extiende a la clase AbstractPageBean. Como dijimos antes, AbstractPageBean tiene varios métodos para manejar el ciclo de vida de la página. Cuatro de estos métodos (init, preprocess, prerender y destroy) son sobrescritos por Page1.java. Excepto por el método init, estos métodos están vacíos al principio. Sirven como receptáculos para que el programador pueda personalizar el comportamiento de su aplicación Web. El archivo de bean de página también incluye métodos establecer (set) y obtener (get) para todos los elementos de la página: page, html, head, body y link para empezar. Para ver estos métodos obtener y establecer, haga clic en el signo más (+) en la línea que dice Creator-managed Component Definition.

www.elsolucionario.net

1120 Capítulo 26 Aplicaciones Web: parte 1

Haga clic en este signo más (+) para mostrar el código oculto que se generó

Figura 26.12 | Archivo de bean de página para Page1.jsp, generado por Java Studio Creator 2.

Paso 6: Cambiar el nombre de los archivos JSP y JSF Por lo general, es conveniente cambiar el nombre de los archivos JSP y Java en un proyecto, de manera que sus nombres sean relevantes para nuestra aplicación. Haga clic con el botón derecho del ratón en el archivo Page1. jsp en la Ventana Proyectos y seleccione Cambiar nombre, para que aparezca el cuadro de diálogo Cambiar nombre. Escriba el nuevo nombre Hora para el archivo. Si está activada la opción Previsualizar todos los cambios, aparecerá la Ventana Refactorización en la parte inferior del IDE cuando haga clic en Siguiente >. La Refactorización es el proceso de modificar el código fuente para mejorar su legibilidad y reutilización, sin modificar su comportamiento; por ejemplo, al cambiar los nombres a los métodos o variables, o al dividir métodos extensos en varios métodos más cortos, Java Studio Creator 2 tiene herramientas de refactorización integradas, las cuales automatizan ciertas tareas de refactorización. Al usar estas herramientas para cambiar el nombre a los archivos del proyecto, se actualizan los nombres tanto del archivo JSP como del archivo de bean de página. La herramienta de refactorización también modifica el nombre de la clase en el archivo de bean de página y todos los enlaces de los atributos en el archivo JSP, para reflejar el nuevo nombre de la clase. Observe que no se hará ninguno de estos cambios, sino hasta que haga clic en el botón Refactorizar de la Ventana Refactorización. Si no previsualiza los cambios, la refactorización ocurre al momento en que haga clic en el botón Siguiente > del cuadro de diálogo Cambiar nombre.

Paso 7: Cambiar el título de la página Antes de diseñar el contenido de la página Web, vamos a darle el título "Hora Web: un ejemplo simple". De manera predeterminada, la página no tiene un título cuando el IDE la genera. Para agregar un título, abra el archivo JSP en modo Diseño. En la ventana Propiedades, escriba el nuevo título enseguida de la propiedad Title y oprima Intro. Vea la JSP para cerciorarse que el atributo title = "Hora Web: un ejemplo simple" se haya agregado automáticamente a la etiqueta ui:head.

www.elsolucionario.net

26.5 Creación y ejecución de una aplicación simple en Java Studio Creator 2 1121

Paso 8: Diseñar la página Diseñar una página Web es más sencillo en Java Studio Creator 2. Para agregar componentes a la página, puede arrastrarlos y soltarlos desde la Paleta hacia la página en modo Diseño. Al igual que la misma página Web, cada componente es un objeto que tiene propiedades, métodos y eventos. Puede establecer estas propiedades y eventos en forma visual, mediante la ventana Propiedades, o mediante programación en el archivo de bean de página. Los métodos obtener (get) y establecer (set) se agregan automáticamente al archivo de bean de página para cada componente que se agregue a la página. El IDE genera las etiquetas JSP para los componentes que el programador arrastra y suelta mediante el uso de un diseño de cuadrícula, como se especifica en la etiqueta ui:body. Esto significa que los componentes se desplegarán en el navegador usando posicionamiento absoluto, de manera que aparezcan exactamente en donde se sueltan en la página. A medida que el programador agregue componentes a la página, el atributo style en el elemento JSP de cada componente incluirá el número de píxeles desde los márgenes superior e izquierdo de la página en la que se posicione el componente. En este ejemplo, usamos dos componentes Texto estático. Para agregar el primero a la página Web, arrástrelo y suéltelo desde la lista de componentes Básicos de la Paleta, hasta la página en modo Diseño. Edite el texto del componente, escribiendo "Hora actual en el servidor Web:" directamente en el componente. También puede editar el texto modificando la propiedad text del componente en la ventana Propiedades. Java Studio Creator 2 es un editor WYSIWYG (What You See Is What You Get —Lo que ve es lo que obtiene); cada vez que realice un cambio a una página Web en modo Diseño, el IDE creará el marcado (visible en modo JSP) necesario para lograr los efectos visuales deseados que aparecen en modo Diseño. Después de agregar el texto a la página Web, cambie al modo JSP. Ahí podrá ver que el IDE agregó un elemento ui:staticText al cuerpo de la página, el cual está enlazado al objeto staticText1 en el archivo de bean de página, y cuyo atributo text coincide con el texto que acaba de escribir. De vuelta al modo Diseño, haga clic en el componente Texto estático para seleccionarlo. En la ventana Propiedades, haga clic en el botón de elipsis enseguida de la propiedad style para abrir un cuadro de diálogo y editar el estilo del texto. Seleccione 18 px para el tamaño de la fuente y haga clic en Aceptar. De nuevo en la ventana Propiedades, cambie la propiedad id a encabezadoHora. Al establecer la propiedad id también se modifica el nombre de la propiedad correspondiente del componente en el bean de página, y se actualiza su atributo binding en la JSP de manera acorde. Observe que se ha agregado fontsize: 18 px al atributo style y que el atributo id ha cambiado a encabezadoHora en la etiqueta del componente en el archivo JSP. El IDE debe aparecer ahora como se muestra en la figura 26.13.

Figura 26.13 |

Hora.jsp

después de insertar el primer componente Texto estático.

www.elsolucionario.net

1122 Capítulo 26 Aplicaciones Web: parte 1

Figura 26.14 |

Hora.jsp

después de agregar el segundo componente Texto

estático.

Coloque un segundo componente Texto estático en la página, y establezca su id en textoReloj. Edite su propiedad style de manera que el tamaño de la fuente sea 18 px, el color de texto sea amarillo (yellow) y el color de fondo sea negro (black). No edite el texto del componente, ya que éste se establecerá mediante programación en el archivo de bean de página. El componente se mostrará con el texto Texto estático en el IDE, pero no mostrará ningún texto en tiempo de ejecución, a menos que éste se establezca mediante programación. La figura 26.14 muestra el IDE después de agregar el segundo componente.

Paso 9: Agregar la lógica de la página Después de diseñar la interfaz de usuario, puede modificar el archivo de bean de página para establecer el texto del elemento textoReloj. En este ejemplo, agregamos una instrucción al método prerender (líneas 170 a 174 de la figura 26.6). Recuerde que utilizamos el método prerender para asegurar que textoReloj se actualice cada vez que se actualice la página. En las líneas 172 y 173 de la figura 26.6 se establece mediante programación el texto de textoReloj con la hora actual en el servidor. Nos gustaría que esta página se actualizara automáticamente para mostrar una hora actualizada. Para lograr esto, agregue la etiqueta vacía al archivo JSP, entre el final de la etiqueta ui:head y el inicio de la etiqueta ui:body. Esta etiqueta indica al navegador que debe volver a cargar la página automáticamente cada 60 segundos. También puede agregar esta etiqueta arrastrando un componente Meta de la sección Avanzados de la Paleta a su página, y después estableciendo el atributo content del componente a 60 y su atributo httpEquiv a refresh.

Paso 10: Análisis de la ventana Esquema En la figura 26.15 se muestra la ventana Esquema en Java Studio Creator 2. Los cuatro archivos Java del proyecto se muestran como nodos de color gris. El nodo Hora que representa el archivo de bean de página está expandido y muestra el contenido del árbol de componentes. Los beans de ámbito de petición, sesión y aplicación están contraídos de manera predeterminada, ya que no hemos agregado propiedades a estos beans en este ejemplo. Al hacer clic en el árbol de componentes de la página, se selecciona el elemento en el Editor visual.

Paso 11: Ejecutar la aplicación Después de crear la página Web, podemos verla de varias formas. Primero seleccione Generar > Generar proyecto principal, y después que se complete la generación, seleccione Ejecutar > Ejecutar proyecto principal para ejecu-

www.elsolucionario.net

26.6

Componentes JSF 1123

Figura 26.15 | Ventana Esquema en Java Studio Creator 2. tar la aplicación en una ventana del navegador. Para ejecutar un proyecto que ya haya sido generado, oprima el icono Ejecutar proyecto principal ( ) en la barra de herramientas que se encuentra en la parte superior del IDE. Observe que, si se hacen cambios a un proyecto, éste debe volver a generarse para que puedan reflejarse cuando se vea la aplicación en un navegador Web. Como esta aplicación se generó en el sistema de archivos local, el URL que se muestre en la barra de dirección del navegador cuando se ejecute la aplicación será http://localhost:29080/HoraWeb/ (figura 26.6), en donde 29080 es el número de puerto en el que se ejecuta el servidor de prueba integrado de Java Studio Creator 2 (Sun Application Server 8) de manera predeterminada. Al ejecutar un programa en el servidor de prueba, aparece un icono cerca de la parte inferior derecha de la pantalla, para demostrar que Sun Application Server se está ejecutando. Para cerrar el servidor después de salir de Java Studio Creator 2, haga clic con el botón derecho en el icono de la bandeja y seleccione Stop Domain creator. De manera alternativa, puede oprimir F5 para generar la aplicación y después ejecutarla en modo de depuración; el depurador integrado de Java Studio Creator 2 puede ayudarle a diagnosticar fallas en las aplicaciones. Si escribe F5, el programa se ejecuta sin habilitar la depuración.

Tip para prevenir errores 26.1 Si tiene problemas al generar su proyecto debido a errores en los archivos XML generados por Java Studio Creator, que se utilizan para la generación, pruebe a limpiar el proyecto y volver a generar. Para ello, seleccione Generar > Limpiar y generar proyecto principal, u oprima B.

Por último, para ejecutar su aplicación generada, abra una ventana del navegador y escriba el URL de la página Web en el campo Dirección. Como su aplicación reside en el sistema de archivos local, primero debe iniciar Sun Application Server. Si ejecutó antes la aplicación utilizando uno de los métodos anteriores, el servidor ya se estará ejecutando. De no ser así, puede iniciar el servidor desde el IDE; para ello abra la ficha Servidores (que se encuentra en el mismo panel que la Paleta), haga clic con el botón derecho del ratón en el Servidor de ejecución, seleccione Iniciar/Detener servidor y haga clic en el botón Iniciar, en el cuadro de diálogo que aparezca. Después, puede escribir el URL (incluyendo el número de puerto para el servidor de aplicación, 29080) en el navegador para ejecutar la aplicación. Para este ejemplo no es necesario escribir el URL completo, http:// localhost:29080/HoraWeb/faces/Hora.jsp. La ruta para el archivo Hora.jsp (es decir, faces/Hora.jsp) se puede omitir, ya que este archivo se estableció de manera predeterminada como la página inicial del proyecto. Para los proyectos con varias páginas, puede modificar la página inicial haciendo clic en la página deseada en la ventana Proyectos, y seleccionando Definir como página de inicio. La página de inicio se indica mediante una flecha verde enseguida del nombre de la página en la ventana Proyectos. [Nota: si utiliza Netbeans Visual Web Pack 5.5, el número de puerto dependerá del servidor en el que despliegue su aplicación Web. Además, la ficha Servidores se llama Tiempo de ejecución (Runtime) en Netbeans].

26.6 Componentes JSF En esta sección presentaremos algunos de los componentes JSF que se incluyen en la Paleta (figura 26.9). En la figura 26.16 se sintetizan algunos de los componentes JSF que se utilizan en los ejemplos del capítulo.

26.6.1 Componentes de texto y gráficos En la figura 26.17 se muestra un formulario simple para recopilar la entrada del usuario. Este ejemplo utiliza todos los componentes enlistados en la figura 26.16, con la excepción de Etiqueta, que veremos en ejemplos

www.elsolucionario.net

1124 Capítulo 26 Aplicaciones Web: parte 1

Componentes JSF

Descripción

Etiqueta

Muestra texto que se puede asociar con un elemento de entrada.

Texto estático

Muestra texto que el usuario no puede editar.

Campo de texto

Recopila la entrada del usuario y muestra texto.

Botón

Desencadena un evento cuando se oprime.

Hipervínculo

Muestra un hipervínculo.

Lista desplegable

Muestra una lista desplegable de opciones.

Grupo de botones de selección

Muestra botones de opción.

Imagen

Muestra imágenes (como GIF y JPG).

Figura 26.16 | Componentes JSF de uso común. posteriores. Todo el código en la figura 26.17 se generó mediante Java Studio Creator 2, en respuesta a las acciones realizadas en modo Diseño. Este ejemplo no realiza ninguna tarea cuando el usuario hace clic en Registrar. Le pediremos que agregue funcionalidad a este ejemplo como un ejercicio. En los siguientes ejemplos, demostraremos cómo agregar funcionalidad a muchos de estos componentes JSF. Antes de hablar sobre los componentes JSF que se utilizan en este archivo JSP, explicaremos el XHTML que crea el esquema de la figura 26.17. Como dijimos antes, Java Studio Creator 2 utiliza el posicionamiento absoluto, por lo que los componentes se despliegan en donde se hayan soltado en el Editor visual. En este ejemplo, además del posicionamiento absoluto utilizamos un componente Panel de cuadrícula (líneas 31 a 52) del grupo de componentes Diseño de la Paleta. El prefijo h: indica que se encuentra en la biblioteca de etiquetas HTML de JSF. Este componente, un objeto de la clase HtmlPanelGrid en el paquete javax.faces.component.html, controla el posicionamiento de los componentes que contiene. El componente Panel de cuadrícula permite al diseñador especificar el número de columnas que debe contener la cuadrícula. Después se pueden soltar los componentes en cualquier parte dentro del panel, y éstos se reposicionarán automáticamente en columnas espaciadas de manera uniforme, en el orden en el que se suelten. Cuando el número de componentes excede al número de columnas, el panel desplaza los componentes adicionales hacia una nueva fila. De esta forma, el Panel de cuadrícula se comporta como una tabla de XHTML, y de hecho se despliega en el navegador como una tabla XHTML. En este ejemplo, usamos el Panel de cuadrícula para controlar las posiciones de los componentes Imagen y Campo de texto en la sección de la página acerca de la información del usuario.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

View more...

Comments

Copyright © 2017 DATENPDF Inc.