Programación orientada a ob jetos en Java 1ª edición
Guadalajara, Jal., México Enero 2004
CONTENIDO
1
Introducción al paradigma orientado a objetos 1.1 Introducción a los objetos : atributos, acciones 1.2 Programación Estructurada vs. Programación Orientada a Objetos 1.3 Característica de los Lenguajes Orientados a Objetos 1.3.1 Abstracción 1.3.2 Encapsulamiento 1.3.3 Modularidad 1.3.4 Jerarquía 1.3.5 Polimorfismo 1.4 Características de un objeto 1.4.1 Identidad 1.4.2 Estado 1.4.3 Comportamiento 1.4.4 Relación entre estado y comportamiento
2
Modelando objetos con UML 2.1 Breve historia de UML 2.2 Representación gráfica de las clases 2.3 Visibilidad de atributos / métodos 2.4 Asociación 2.5 Agregación 2.6 Relación muchos a muchos con Asociación/Agregación 2.7 Herencia 2.8 Ejemplos
3
Fundamentos de Java 3.1 Origen de Java 3.2 Características de Java 3.2.1 Simple 3.2.2 Orientado a objetos 3.2.3 Distribuido 3.2.4 Robusto 3.2.5 Arquitectura neutral 3.2.6 Multiplataforma 3.3 Programación básica
II PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
3.3.1 Comentarios 3.3.2 Identificadores 3.3.3 Tipos de datos y literales 3.3.3.1 Enteros 3.3.3.2 Reales 3.3.3.3 Lógicos 3.3.3.4 Caracter 3.3.3.5 Cadenas de caracteres 3.3.4 Operadores 3.3.4.1 Aritméticos 3.3.4.2 Relacionales 3.3.4.3 Condicionales (lógicos) 3.3.4.4 A nivel de bits 3.3.4.5 De asignación 3.3.5 Separadores 3.4 Impresión en pantalla 3.5 Control de flujo 3.5.1 Sentencias de salto 3.5.1.1 Sentencia if 3.5.1.2 Sentencia switch 3.5.2 Sentencias de bucle 3.5.2.1 Ciclo for 3.5.2.2 Ciclo while 3.5.2.3 Ciclo do / while 3.6 Conversión de tipos 4
Estructura de un programa 4.1 Encabezado 4.2 Definición de la clase 4.3 Declaración de atributos 4.4 Declaración de métodos 4.4.1 Invocación de métodos y sentencia return 4.4.2 Constructores 4.4.3 Método main 4.5 Ámbito de una variable 4.6 Traducción de una clase en notación UML a una clase en código Java
5
Objetos 5.1 Creación 5.2 Uso de atribut os y métodos de clase 5.3 Alcance de un objeto 5.4 Pase por valor / referencia
6
Modificadores 6.1 Para atributos 6.2 Para métodos 6.3 Para clases 6.3.1 Clases no derivables / no instanciables 6.4 Representación de modificadores utilizando notación UML
CONTENIDO III
7
Almacenamiento de datos 7.1 Arreglos 7.1.1 Declaración 7.1.2 Instanciación 7.1.3 Manipulación 7.1.4 Ejemplo 7.1.5 Declaración / instanciación / rellenado 7.1.6 Igualación 7.2 Vectores 7.2.1 Declaración / creación 7.2.2 Adición de elementos 7.2.3 Navegación 7.2.4 Eliminación de elementos 7.2.5 Ejemplo 7.3 Tablas hash 7.3.1 Declaración y creación 7.3.2 Adición de elementos 7.3.3 Obtención de elementos 7.3.4 Eliminación de elementos 7.3.5 Ejemplo 7.4 Implementación de la asociación de clases 7.5 Implementación de la agregación de clases 7.6 Ejemplo final
8
Herencia y polimorfismo 8.1 Introducción 8.2 Especialización 8.2.1 Clase base 8.2.2 Objetos de la clase base 8.2.3 Clase derivada 8.2.4 Objetos de la clase derivada 8.3 Generalización 8.3.1 Clase abstracta 8.3.2 Clases derivadas 8.3.3 Objetos de una clase derivada 8.3.4 Enlace dinámico 8.3.5 Polimorfismo en acción 8.4 Herencia múltiple 8.4.1 Interfaces 8.4.2 Clase implementadora 8.4.3 Herencia múltiple en acción 8.4.4 Polimorfismo en acción 8.4.5 Modelado de interfaces usando notación UML
9
Excepciones 9.1 Excepciones comunes 9.2 Capturando excepciones 9.2.1 Capturando varias excepciones 9.3 Creando excepciones propias
IV PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
9.3.1 Métodos que lanzan excepciones 9.3.2 Captura de las excepciones propias 9.4 Ciclo de vida de una excepción 10 Hilos 10.1 Flujos de un programa 10.2 Creación de un hilo 10.3 Estados de un hilo 10.3.1 Nuevo 10.3.2 En ejecución 10.3.3 Suspendido 10.3.4 Muerto 10.4 Comunicación entre hilos 11 Interfaz gráfica de usuario 11.1 AWT 11.2 Swing 11.3 Creación y configuración de los componentes visuales básicos de Swing 11.3.1 JLabel 11.3.2 JTextField 11.3.3 JButton 11.3.4 JList 11.3.5 JScrollPane 11.3.6 JComboBox 11.3.7 Cuadros de diálogo 11.4 Adición de los componentes visuales básicos de Swing en una aplicación 11.4.1 JFrame 11.4.2 JPanel 11.4.3 Disposición de componentes Swing en un Jpanel 11.4.3.1 FlowLayout 11.4.3.2 GridLayout 11.4.3.3 BorderLayout 11.5 Manejo de eventos con componentes Swing 11.5.1 JButton 11.5.2 JList
CAPÍTULO 1
INTRODUCCIÓN AL PARADIGMA ORIENTADO A OBJETOS
Adonde quiera que volteemos podemos observar que el mundo real está compuesto por objetos: personas, animales, plantas, edificios, vehículos, etc., lo que ocasiona que el ser humano piense en términos de objetos. Por esta razón, la orientación a objetos es una forma más natural de pensar y, por tanto, la codificación de programas utilizando el paradigma orientado a objetos se convierte en un proceso más natural que el paradigma estructurado tradicional. El objetivo de la programación orientada a objetos es reducir la distancia entre el razonamiento humano y el lenguaje de los ordenadores , como se puede observar en la Figura 1. Objetos
Programación más abstracta
Tipos de datos abstractos Simplificación
Funciones Mnemónicos Código binario
Programación más difícil
Figura 1 Evolución de la programación
1.1 Introducción a los objetos: atributos, acciones Podemos dividir a los objetos en 2 categorías: animados e inanimados. Los objetos animados están “vivos” en algún sentido ya que pueden realizar acciones (desplazarse, emitir ruidos, comer…) bajo su propia iniciativa. Los objetos inanimados solamente están ahí, esperando a que un objeto animado interactúe con él. Sin embargo, todos los objetos tienen algo en común: cuentan con un conjunto de atributos –forma, color, tamaño, peso, edad, posición– y un conjunto de acciones que pueden realizar y que definen su comportamiento –una pelota rueda (como respuesta a otra acción), rebota, se infla, se desinfla, un carro avanza, frena, un bebé llora, duerme, gatea–. Nótese que, aunque una pelota no tenga vida propia, también puede realizar acciones que, por lo general, suceden como consecuencia de la interacción con otros objetos.
2 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
El ser humano aprende sobre los objetos estudiando sus atributos y observando sus comportamientos. Gracias a su capacidad de abstracción , puede notar que varios objetos diferentes pueden exhibir atributos y comportamientos similares, y es capaz de clasificarlos en categorías (clases).
1.2 Programación Estructurada vs. Programación Orientada a Objetos La programación orientada a objetos nos proporciona una forma más natural e intuitiva de ver el proceso de programación, ya que simula objetos del mundo real utilizando equivalentes de software. En C, Pascal, Cobol y otros lenguajes de programación estructurada, la programación tiende a estar orientada a la acción, mientras que la programación en Java, Eiffel, SmallTalk y C++, entre otros, están orientada al objeto . En C, la unidad de programación es la función. En Java, la unidad de programación es la clase, a partir de la cual los objetos son creados en algún momento. Los programadores de C se concentran en escribir funciones, las cuales se agrupan para formar programas. Los datos son importantes pero solamente como apoyo de las acciones efectuadas por las funciones. Los programadores de Java se concentran en definir sus propios tipos de datos, llamados clases. Los verbos de la especificación de un sistema ayudan al programador en C a determinar el conjunto de funciones con el que se implementará tal sistema. Los sustantivos de la especificación de un sistema son la base para que el programador de Java determine el conjunto de clases que componen su sistema.
1.3 Característica de los lenguajes orientados a objetos (LOO) Para que un lenguaje de programación sea considerado orientado a objetos debe cumplir con las características definidas a continuación.
1.3.1 Abstracción La abstracción consiste en separar las propiedades más importantes de las que no lo son. Gracias a la abstracción podemos definir las características esenciales de los objetos reales a través de atributos y comportamientos, y clasificarlos en categorías llamadas clases. Una clase es una descripción genérica de un grupo de objetos que comparten propiedades comunes. Todo LOO deberá permitir la implementación de clases a partir de los cuales podemos crear muchos objetos con atributos y comportamientos afines. cuchillo, cuchara, tenedor Hugo, Paco, Luis 15, x, 3.5, PI ‘a’, ‘e’, ‘i’ Pointer, Ibiza, Corsa
: : : : :
Cubierto Pato Número Vocal Automóvil
1.3.2 Encapsulamiento En el concepto de la POO lo que realmente nos importa es el comportamiento de los objetos, no cómo está implementado tal comportamiento; esto es, para invocar alguna acción desempeñada por un objeto es necesario conocer únicamente sus parámetros de entrada y de salida, no tanto la forma en que tal objeto resuelve la acción. De esta manera, si la implementación cambia pero su interfaz permanece igual, es decir, la forma como ese objeto se relaciona con el mundo exterior, los objetos que interactúan
CAPÍTULO 1: INTRODUCCIÓN AL PARADIGMA ORIENTADO A OBJETOS 3
con él no se verán afectados por esos cambios. Los objetos tienen la propiedad de ocultamiento de información: aunque un objeto tenga la capacidad de comunicarse con los demás objetos, no conoce la manera en que implementan esos objetos. En resumen, el encapsulamiento consiste en ocultar los detalles de la implementación de un objeto, a la vez que se proporciona una interfaz pública por medio de sus métodos permitidos. Ejemplo: es posible manejar un automóvil sin saber como funcionan internamente el motor, la transmisión y el sistema de escape.
1.3.3 Modularidad Una vez representada una situación del mundo real en un programa, tenemos como resultado un conjunto de objetos de software que constituyen la aplicación. La modularidad nos permite poder modificar las características de la clase que definen a un objeto, de forma independiente de las demás clases en la aplicación. En otras palabras, si nuestra aplicación puede dividirse en módulos separados (normalmente clases) y estos módulos pueden compilarse y modificarse sin afectar a los demás, entonces dicha aplicación ha sido implementada en un lenguaje de programación que soporta la modularidad. Calculadora
Geometría Trigonometría Aritmética
Triángulo Círculo Seno Coseno Suma Resta
Figura 2 Estructura de clases de una aplicación
Supongamos que el sistema representado en la figura 2 fue desarrollado en un LOO que soporta modularidad y que la clase Seno solamente calcula el seno de valores dados en radianes; entonces nosotros podemos modificar la clase Seno de tal manera que acepte valores en grados y radianes sin tener que modificar y/o volver a compilar el resto de la aplicación.
1.3.4 Jerarquía Existen dos tipos de jerarquías de clases en la programación orientada a objetos: 1. “es un tipo de ” : herencia 2. “es parte de” : agregación La herencia nos permite definir una clase a partir de otra ya existente; la nueva clase –clase derivada o subclase– heredará las características y el comportamiento de la clase existente –clase base o superclase– pero además podrá tener atributos y acciones particulares. Por esta razón se dice que la clase derivada extiende a la clase base. Existen 2 tipos de herencia: simple y múltiple. La herencia simple nos permite crear una clase a partir de otra ya existente. La herencia múltiple nos permite definir una clase a partir de 2 o más clases preexistentes. No todos los LOO permiten la herencia múltiple. Java suele implementar la herencia múltiple a través del uso de interfaces y polimorfismo [véase Sección 8.4]. La Figura 3 despliega una estructura de árbol que representa un caso típico en el que se aplica la herencia: las literales [véase Sección 3.3.3]. Cabe mencionar que tal estructura no sigue alguna notación en particular. Una variable en cualquier lenguaje de programación debe tener asignada una literal que indique la manera en que se
4 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
puede utilizar dicha variable. Normalmente encontramos 3 tipos de literales: numéricos, lógicos y alfanuméricos. Una variable con literal numérica asignada podrá participar en operaciones matemáticas. Una literal numérica tiene 2 representaciones: real y entero. En Java, existen 2 tipos de datos para las literales reales: float y real; la diferencia entre estos 2 tipos de datos estriba en el tamaño. En resumen, float tiene las características de Real ya que contiene decimales, de Numérica ya que se pueden realizar operaciones matemáticas con ella, y de Literal ya que puede ser asignado a una variable. float Real real
Numérica Entero Literal
Lógica Cadena de texto Alfanumérica Caracter
Figura 3 Herencia existente entre las literales
La agregación consiste en el agrupamiento lógico de objetos relacionados (agregados) dentro de una clase (agregación). La agregación modela relaciones de tipo: amo / esclavo, todo / partes, en donde una clase está contenida en otra y su existencia depende de la otra. Esto es, cuando la clase agregación deja de existir, todos sus agregados desaparecen consigo [véase Sección 2.5]. La Figura 4 despliega un ejemplo de agregación, en donde la clase Coche contiene las clases Motor, Volante y Pedal; el motor, a su vez contiene la clase bujía. Si el coche desaparece, todos sus agregados hacen lo propio. Coche Motor Bujía
Volante Pedal
Figura 4 Agregación existente en un Coche
1.3.5 Polimorfismo En ocasiones nos encontramos con el caso de que un conjunto de objetos realizan una misma acción pero la llevan a cabo de diferente manera. Por ejemplo, todos los animales pueden desplazarse , pero cada animal lo realiza de una manera muy particular: algunos lo hacen volando, otros arrastrándose, otros nadando, etc. Pero finalmente la acción desplazarse les permitirá cambiar su posición. El polimorfismo le permite a una entidad adoptar una variedad de representaciones. En POO, el polimorfismo consiste en tratar a un objeto de diferentes maneras dentro de una función que no conoce la clase del objeto, sino sólamente a la clase base abstracta o la interfaz que implementa su clase. Para lograr este cometido, Java proporciona interfaces y clases abstractas útiles para especificar las acciones que un conjunto de objetos podrán realizar, cada uno, de una manera particular. Para información más detallada de polimorfismo, clases abstractas e interfaces, consultar capítulos 8.3 y 8.4.
CAPÍTULO 1: INTRODUCCIÓN AL PARADIGMA ORIENTADO A OBJETOS 5
1.4 Características de un objeto En la programación orientada a objetos, un objeto no representa sólamente una agrupación de datos y de código, sino que también se caracteriza por tener: estado, comportamiento e identidad.
1.4.1 Identidad La identidad caracteriza la existencia de un objeto, esto es, distingue de manera unívoca a un objeto de los demás; cada objeto posee una identidad de manera implícita (inherente). Esta identidad no se representa de manera específica en modelado ya que los LOO internamente gestionan la identidad de todos los objetos creados.
1.4.2 Estado El estado representa el conjunto de los valores de todos los atributos de un objeto en un instante dado de su existencia. El estado de un objeto evoluciona con el tiempo. Mi coche
Azul 1500 km. 10 lt.
Figura 4 Ejemplo de un objeto que guarda un estado
En la Figura 4 observamos un objeto “Mi coche” el cual, en un momento dado de su existencia, tiene asignados los valores “Azul”, 1500 y 10 para los atributos Color, Kilometraje y Gasolina, respectivamente, pertenecientes a la clase Coche. Estos valores en su conjunto determinan el estado en que se encuentra el coche. Si el coche se mantiene en marcha, la cantidad de gasolina disminuirá, el kilometraje aumentará y, por lo tanto, el estado cambiará.
1.4.3 Comportamiento El comportamiento de un objeto agrupa todas sus competencias, es decir, todas las acciones que puede realizar; a la unidad mínima de comportamiento se le denomina operación o método. Los métodos suceden como consecuencia de un estímulo externo: mensaje [véase Sección 5.2]. La Figura 5 despliega todas las acciones que puede realizar el objeto “Mi coche”, las cuales definen su comportamiento. Mi coche
Arrancar( ) Frenar ( ) Acelerar ( )
Figura 5 Ejemplo de un objeto que exhibe un comportamiento
1.4.4 Relación entre estado y comportamiento El estado y el comportamiento están íntimamente relacionados. Esto es, el comportamiento en un instante dado depende del estado actual, y el estado puede ser modificado por el comportamiento. Lo
6 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
anterior se puede apreciar gráficamente en la Figura 6, donde Persona1 envía mensajes a Micoche indicándole la acción que deberá realizar y que modificará su estado; de acuerdo al estado en que se encuentra Micoche es el conjunto de acciones que podrá efectuar.
Figura 6 Ejemplo de relación de estado y comportamiento
CAPÍTULO 2
MODELANDO OBJETOS CON UML
En esta capítulo se presenta UML [Unified Modeling Language], una notación gráfica útil para el modelado de sistemas de software utilizando el paradigma orientado a objetos.
2.1 Breve historia de UML Debido a la proliferación de los lenguajes de POO durante los años 90’s, comenzaron a surgir una gran variedad de notacones para representar gráficamente a los objetos y todo lo que tenga que ver con ellos: mensajes, relaciones, acciones, asociaciones, etc. Entre todos los autores que propusieron sus notaciones gráficas, podemos destacar a Jim Rumbaugh y Grady Booch. A finales de 1994, decidieron unificar sus trabajos en un lenguaje único: el método unificado. Un año más tarde, se les unió Ivar Jacobson, el creador de los casos de uso : una técnica eficaz para la determinación de necesidades. A partir de ahí, surgieron algunas versiones posteriores del método unificado. Finalmente, se transformó en UML –the unified modeling language for object-oriented development– con el objetivo de definir un lenguaje universal y simple para modelar todo tipo de objetos. Pero, ¿para qué queremos modelar? Un modelo es una representación o descripción general de un sistema o un proceso. En el caso de UML, tal descripción es gráfica. Modelar nos permite comprender con mayor facilidad la organización y la operación de los sistemas complejos. UML proporciona 9 diferentes tipos de diagramas útiles para representar los diferentes aspectos de un sistema durante la etapa de análisis y diseño. El modelado con UML comienza a partir de la obtención de los requerimientos del sistema hasta la codificación de módulos del sistema. 1. 2. 3. 4. 5. 6. 7. 8. 9.
Casos de uso Clases Objetos Colaboración Secuencia Estado Actividades Componentes Despliegue
Modelado de las necesidades del sistema Componentes lógicos del sistema (parte estática del sistema) Interacción de tales componentes Parte dinámica del sistema Parte física del sistema
El presente capítulo se enfocará en presentar la notación de los diagramas de clases únicamente.
8 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
2.2 Representación gráfica de las clases Una clase se representa en UML utilizando un rectángulo dividido en tres compartimentos: uno para especificar el nombre de la clase, otro para sus atributos, y el último para las operaciones.
Figura 7 Ejemplos de descripción gráfica de clases
En la Figura 7 se puede observar que todo objeto de tipo Automóvil se caracteriza por tener una marca, modelo, color, gasolina disponible y kilómetros recorridos. Además un objeto Automóvil puede encenderse, apagarse, acelerar, desacelerar y frenar; el método acelerar() ocasiona decrementos e incrementos constantes en los valores de los atributos gasolina y kilometraje, respectivamente. De manera similar, un Cronómetro tiene 5 atributos y 5 métodos. Los métodos del cronómetro podrán cambiar constantemente el valor de sus atributos. Por ejemplo, poner_ceros() asigna el valor 0 a los atributos centésimas , segundos y minutos; iniciar_conteo() cambia el valor de Estado y ocasionará incrementos sucesivos en los valores de los atributos anteriores.
2.3 Visibilidad de atributos / métodos Como se puede observar en la Figura 7, todos los atributos están precedidos por un signo –, y los métodos por un signo +. Estos signos representan el nivel de visilibidad que se asignan a los atributos y métodos. C++ propone tres niveles de visibilidad: 1. Privado : un atributo privado solamente puede ser leído y modificado dentro de la clase que lo contiene; de manera similar, un método privado solamente puede ser invocado dentro de la clase que lo contiene; en la notación UML, el nivel privado se representa con el signo – 2. Protegido : un atributo o método protegido puede ser leído/invocado dentro de la clase que lo contiene y dentro de las clases derivadas de la clase proveedora [véase Capítulo 8]; el nivel protegido se representa con el signo # en UML 3. Público: todos los objetos tienen acceso a los atributos y métodos públicos de una clase; se rompe la noción de visibilidad; se modela con el signo + en la descripción de clases De modo predeterminado, los atributos son privados (ocultos) y los métodos públicos (visibles). En las capítulos siguientes se verán ejemplos de codificación de clases a partir de la descripción gráfica utilizando notación UML.
CAPÍTULO 2: MODELANDO OBJETOS CON UML 9
2.4 Asociación La asociación es una conexión semántica bidireccional entre clases. A menudo viene acompañada de una forma verbal activa o pasiva. El sentido de lectura se especifica con los caracteres <, >.
Figura 8 Ejemplo 1 de asociación entre clases utilizando formas verbales
Podemos agregar una etiqueta en la conexión para especificar el tipo de objeto que participa en la relación. Nótese en la Figura 9 que tal etiqueta se escribe en cursiva.
Figura 9 Ejemplo 2 de asociación entre clases utilizando etiquetas
Las relaciones suelen contener también información de multiplicidad, esto es, el número de instancias [véase Sección 5.1] participantes en una relación. Los valores de multiplicidad más comunes son: • • • • • •
1 0, 1 m .. n m, n 0 .. * 1 .. *
Uno y sólo uno Cero o uno De m a n; donde m y n son números naturales incluyendo al 0 m ó n; donde m y n son números naturales incluyendo al 0 De cero a varios; se puede utilizar también * De uno a varios; se puede utilizar también +
Figura 10 Ejemplo 3 de asociación entre clases utilizando multiplicidad
En el ejemplo de la Figura 10 podemos apreciar que a cada Factura le corresponde uno o más Productos, y un Cliente; además, a cada Cliente y a cada Producto le corresponden cero o más Facturas. Usando palabras más cotidianas, mediante una Factura se pueden vender uno o más productos a un Cliente; un mismo Producto puede venderse más de una vez o no venderse; se pueden emitir 0 o más facturas a un mismo Cliente. Nótese que para que exista una Factura, debe existir previamente al menos un Cliente y un Producto; en caso contrario, la Factura no puede crearse. Además, si se elimina una Factura, no se tienen por qué eliminar el Cliente comprador y los Productos incluidos en la factura.
10 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
2.5 Agregación La agregación es una forma particular de asociación que expresa el acoplamiento entre clases, esto es, una clase se encuentra contenida en otra. La agregación modela relaciones de tipo: amo/esclavo, y todo/partes. Para representarlo gráficamente, se agrega un rombo del lado de la clase contenedora o agregación , como se puede apreciar en el diagrama de clases de la Figura 11 (lado izquierdo). :Agregación por contención física
Agregación
1
*
Componente
=
:Componente
:Componente
Figura 11 Representación de la agregación utilizando notación UML
El diagrama del lado derecho de la Figura 11 muestra otra forma de representar la agregación utilizando objetos en lugar de clases. Esta forma se denomina agregación por contención física , esto es, un objeto contiene físicamente a otros objetos. Podemos distinguir un objeto de una clase porque su nombre está subrayado en el diagrama y comienza con el caracter “:”.
Alarma
1..4
1
Reloj
1
:Reloj 1
Cronómetro
:Alarma
:Cronómetro
Figura 12 Ejemplo 1 de agregación entre clases y por contención física
El diagrama de clases de la Figura 12 representa un reloj que puede contener de uno a cuatro alarmas y un solo cronómetro. Nótese que la alarma y el cronómetro se modelan también como clases porque cada uno de ellos contienen a su vez atributos y métodos que los caracterizan. Del lado derecho de la Figura 12, se modela por contención física un objeto de tipo Reloj que cumple con la especificación del diagrama de clases, ya que este reloj contiene un objeto de tipo alarma y uno de tipo cronómetro. :Coche Coche
1
Motor
2, 3
Pedal
4, 6, 8
Bujía
:Motor
:Pedal
:Bujía
:Bujía
:Pedal
:Bujía
:Bujía
Figura 13 Ejemplo 2 de agregación entre clases y por contención física
La figura 13 modela un coche como una agregación de un motor y 2 ó 3 pedales. El motor a su vez puede contener 4, 6 ó 8 bujías. Nótese que no se especifica multiplicidad en la relación del lado de la agregación; en estos casos, la multiplicidad predeterminada es 1. En este curso sólamente se modelarán y codificarán relaciones de agregación que tengan multiplicidad 1 del lado de la clase agregación; lo anterior quiere decir que una clase agregado estará contenido dentro de una sóla clase agregación, y no dentro de 2 ó más.
CAPÍTULO 2: MODELANDO OBJETOS CON UML 11
A diferencia de la asociación, la agregación no exige la existencia previa de un objeto agregado para poder crear el objeto agregación . Esto es, cuando se crea la agregación, se crean todos los agregados consigo; asimismo, cuando se elimina la agregación, se eliminan todos los agregados: si destruimos un coche, se destruyen tambien sus pedales, el motor y las bujías.
2.6 Relación muchos a muchos con Asociación/Agregación Durante el modelado de sistemas, a menudo nos encontramos con relaciones entre clases de tipo muchos a muchos , en los cuales a cada objeto de tipo P le corresponden varios de tipo Q, y a cada objeto de tipo Q le corresponden varios de tipo P. En el ejemplo de la Figura 10 encontramos que las relaciones Factura–Producto y Factura–Cliente son de tipo muchos a muchos . Para poder codificar este tipo de relaciones en el lenguaje Java con las fórmulas presentadas en los Capítulos 7.4 y 7.5, es necesario transformarlas en relaciones 1 a 1, ó 1 a muchos; para lograr este objetivo, creamos una clase intermedia, intercambiamos los valores de multiplicidad y combinamos los conceptos de asociación y agregación como se puede apreciar en la figura 14. Clase A
+
Clase A
*
+
Clase B
*
Clase AB
1
Relación muchos a muchos eliminada
Clase B
Figura 14 Modelado de la relación muchos a muchos combinando asociación y agregación
La fórmula representada en la Figura 14 podrá entenderse mejor analizando el ejemplo de la Figura 15. La relación Factura–Producto es de manera inherente muchos a muchos , por lo tanto, es necesario transformar o “bajar de nivel ” tal relación para poder llevarla a la codificación. Nótese que toda Factura contiene uno o más productos vendidos, los cuales se irán creando durante la generación de la misma; además, si eliminamos la factura, se eliminarán consigo todos los productos vendidos que fueron creados; lo anterior denota una típica relación de agregación; por lo tanto, creamos una clase intermedia Prod_vendido la cual será un agregado de la clase Factura, esto es, una factura contendrá uno o más objetos de tipo Prod_vendido . Factura
Factura
:Factura :Prod_Vendido :Prod_Vendido :Prod_Vendido :Prod_vendido
+
1
1
1
*
+
Producto
*
Prod_vendido
1
Producto
:Factura
:Producto :Producto
1
1
1
:Prod_Vendido :Prod_Vendido
:Producto
Figura 15 Ejemplo de modelado de la relación muchos a muchos combinando asociación y agregación
12 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
Nótese también que cada objeto Prod_vendido tiene asociado un objeto de tipo Producto , el cual deberá existir previamente; además, cada objeto Producto podrá estar incluido en muchas Facturas, esto es, podrá ser asignado a varios objetos de Prod_vendido. Lo anterior denota una relación de asociación que nos servirá para obligar la previa existencia de productos para crear una factura y prevenir la eliminación de productos como consecuencia de la eliminación de Facturas. En la parte inferior de la Figura 15, se modelan objetos –utilizando agregación por contención física– de tipo Factura , Producto y Producto_vendido cuyas relaciones cumplen con la especificación del diagrama de clases.
2.7 Herencia Las relaciones de herencia se modelan con UML utilizando árboles, donde cada relación contiene una flecha que apunta siempre a la clase más general: la superclase . Existen 2 puntos de vista en las jerarquías de clases: especialización y generalización. La especialización consiste en crear una clase a partir de otra ya existente; la nueva clase (subclase) herederá y extenderá el comportamiento definido en la clase existente (superclase) [véase Sección 8.2]. En la generalización, se forma una clase más general a partir de un conjunto de clases que exhiben una serie de elementos en común. El Sección 8.3 introduce las clases abstractas y el polimorfismo, los cuales son producto de la generalización. Pedal
Automóvil
1
Automático
Estándar
Acelerador
Clutch
1 2
1
Freno 1
1 2
1
Figura 16 Ejemplos de herencia de clases
En la Figura 16 podemos observar 2 ejemplos de herencia de clases. Del lado derecho de la figura apreciamos que existen 3 tipos de pedales: acelerador, clutch y freno; mientras que del lado izquierdo notamos que existen 2 tipos de automóviles: automático y estándar. Además, todo automóvil contiene un acelerador y un freno, y todo automóvil estándar tiene un clutch. Nótese que debido a que Estándar hereda de Automóvil, todo objeto Estándar también contiene un objeto Acelerador y un objeto Freno. Cabe mencionar que en los diagramas de clases podemos usar conectores de la misma manera que como se utilizan en los diagramas de flujo tradicionales: para atender el problema de los espacios de trabajo reducidos.
2.8 Ejemplos Esta sección está encaminado a presentar varios ejemplos de diagramas de clases utilizando la notación UML y describiendo previamente y de manera textual el problema a modelar. El diagrama de la Figura 17 modela el siguiente escenario: un grupo de cierta universidad se caracteriza por tener asociado una materia, un maestro, uno o dos salones, y al menos 5 y no más de 30 alumnos inscritos. Una materia puede impartirse en varios grupos a la vez, o no impartirse. Una materia tiene asignado una especialidad y cada especialidad se asigna al menos a una materia. Un salón siempre
CAPÍTULO 2: MODELANDO OBJETOS CON UML 13
tendrá al menos un grupo asignado, pero puede tener más. Existen 2 tipos de salones: las aulas y los laboratorios. Un maestro puede dar un grupo o más, o quedarse sin trabajar. Existen 2 tipos de maestros: los adjuntos y los asociados. Un alumno deberá estar inscrito en al menos 3 grupos, pero en no más de 6. Aula
Laboratorio
Salón 1, 2 +
Maestro
1
*
Grupo
* 3..6
1
Materia + 1
5 . . 30
Especialidad
Inscrito Ad unto
Asociado
3..6 1
Alumno
5 . . 30
Figura 17 Modelado de un sistema escolar
En la Figura 18 se presenta el diagrama de clases que modela el siguiente problema: un viaje de cierta línea camionera se caracteriza por tener asignado un camión, uno o dos choferes, un origen y un destino. Además, en un viaje se pueden realizar hasta 3 paradas, o ninguna. Todas las ciudades registradas pueden ser origen, destino o lugar de parada en diferentes viajes, pero no en todas las ciudades se hacen paradas. Un chofer contratado puede realizar muchos viajes, o estar inactivo en un momento dado. Para que el viaje se lleve a cabo, es necesario juntar al menos a 5 pasajeros, y a un máximo de 36. Un camión puede utilizarse para uno o más viajes, o quedar inactivo en un momento dado. Un camión puede contener 32 ó 36 asientos. Cada pasajero tiene asignado un asiento en un viaje dado, y cada asiento podrá tener a un pasajero o estar vacío. Un cliente de la línea camionera podrá ser pasajero en uno o más viajes. La figura 19 describe el siguiente panorama: en una agencia de automóviles, un cliente puede comprar uno o más productos a través de una orden de compra. Una orden tiene asociada solamente un cliente y contiene al menos un detalle de compra; en cada detalle se especifica un producto vendido al cliente. Los productos pueden ser automóviles, refacciones o servicios. Por otro lado, un cliente que emitió una orden de compra podrá efectuar más de un pago para saldar su deuda. Cada pago del cliente deberá tener asociado una orden de compra al cual está abonando. Cada pago se puede llevar a cabo de 3 formas diferentes: en efectivo, en cheque o con tarjeta de crédito.
14 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
Ciudad
Destino
Origen 1
Parada 0..3
1 +
Chofer
1, 2
*
*
+
Viaje
*
1
Camión
+ 5, 36
Cliente
32, 36
5, 36 1
Pasajero
+
0..1
1
Asiento
Figura 18 Modelado de un sistema de viajes
Cliente
Orden
Pago
Crédito
Efectivo
Detalle
Cheque
Automóvil
Producto
Refacción
Figura 19 Modelado del sistema de una agencia de automóviles
Servicio
CAPÍTULO 3
INTRODUCCIÓN AL LENGUAJE JAVA
Java es un lenguaje de programación orientado a objetos desarrollado por la empresa Sun MicroSystems. El objetivo principal de Java es permitir el desarrollo de aplicaciones distribuidas en redes heterogéneas como Internet; para permitir tal objetivo, Java resuelve los problemas de incompatibilidad inherentes entre las diferentes arquitecturas de hardware, sistemas operativos y sistemas de ventanas existentes en el mercado.
3.1 Origen de Java A finales de los ochentas, la empresa Sun MicroSystems inicia un proyecto de investigación encabezado por James Gosling con el propósito de desarrollar un lenguaje de programación para dispositivos electrónicos como tostadoras, hornos microondas y asistentes digitales personales. Gosling y su equipo de investigación llegaron a la conclusión de que el software para dispositivos de consumo tiene algunos requerimientos de diseño únicos. Por ejemplo, el software necesita ser capaz de trabajar en nuevos chips de computadora. Cuando los chips son introducidos, los fabricantes más de una vez los cambian por otros por ser más baratos o introducir nuevos mecanismos. El software también necesita ser extremadamente inteligente, porque cuando un producto del consumidor falla, el fabricante usualmente tiene que reemplazar todo el dispositivo y no el componente que originó el fallo. Gosling y su equipo también descubrieron que existían lenguajes de programación como C y C++ con los cuales no se podía realizar la tarea de hacer un software que fuera independiente de la arquitectura en donde se esté ejecutando. Un programa escrito en C o C++ se compila para un chip de computadora particular. Cuando se cambia de chip, el programa debe ser recompilado. Como resultado de lo dicho anteriormente, en 1990 Gosling comenzó a diseñar un nuevo lenguaje de programación que fuera más apropiado para dispositivos que utilizan software electrónico. Este lenguaje fue conocido originalmente como Oak. Fue pequeño, confiable e independiente de la arquitectura. En 1993, cuando el equipo de Java continuaba trabajando en el diseño del nuevo lenguaje, surgió la Word Wide Web. El equipo de Java consideró que un lenguaje de arquitectura neutral sería ideal para programar en la Internet, porque permitiría que los programas desarrollados fueran ejecutados en cualquier tipo de computadora conectada. Finalmente, el lenguaje Java se presentó como total en agosto de 1995 y encontró en Internet un ámbito de desarrollo de software con posibilidades de disputar la supremacía de Microsoft.
16 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
3.2 Características de Java Sun describe al lenguaje Java de la siguiente manera: simple, orientado a objetos, distribuido, robusto, de arquitectura neutral y multitarea.
3.2.1 Simple Java ofrece toda la funcionalidad de un lenguaje potente, pero sin las características menos usadas y más confusas de éstos. Aprovechando que C y C++ son los lenguajes más difundidos, Java se diseñó para ser parecido a C++ y así facilitar el aprendizaje. Java elimina muchas de las características de otros lenguajes como C++ para mantener reducidas las especificaciones del lenguaje y añadir características muy útiles como el reciclador de memoria dinámica (garbage collector). No es necesario preocuparse por liberar la memoria, el reciclador se encarga de ello y, como es de baja prioridad, permite liberar bloques de memoria muy grandes cuando entra en acción. Además, Java reduce en un 50% los errores más comunes de programación al eliminar muchas características de éstos, entre las que destacamos: aritmética de punteros, macros, definición de tipos, registros, referencias y necesidad de liberar memoria.
3.2.2 Orientado a objetos Java implementa la tecnología básica de C++ con ciertas mejoras y elimina algunas cosas para mantener el objetivo de la simplicidad del lenguaje. Java soporta las tres características propias del paradigma de la orientación a objetos: encapsulamiento, hernecia y polimorfismo. Como en C++, las plantillas de objetos son llamadas clases y sus copias, instancias . Estas instancias necesitan ser construidas y destruidas en espacios de memoria.
3.2.3 Distribuido Java se ha construido con extensas capacidades de interconexión TCP/IP. Java en sí no es distribuido, sino que proporciona herramientas para que los programas puedan ser distribuidos; esto es, existen librerías de rutinas para acceder e interactuar con protocolos como http y ftp, que permiten a los programadores acceder a la información a través de la red con tanta facilidad como a los archivos locales.
3.2.4 Robusto Java realiza verificaciones en busca de problemas tanto en tiempo de compilación como en tiempo de ejecución. La comprobación de tipos en Java ayuda a detectar errores en el ciclo de desarrollo. Java maneja internamente la memoria para eliminar las preocupaciones por parte del programador en cuanto a la liberación o corrupción de la memoria.
3.2.5 Arquitectura neutral El compilador de Java genera un código objeto ( ByteCode) cuyo formato es independiente a la arquitectura de la máquina en que se ejecutará; cualquier máquina que tenga instalada la máquina virtual de Java (JVM) puede ejecutar tal código intermedio sin importar que éste haya sido generado en otra arquitectura de hardware, otro sistema operativo u otro sistema de ventanas. Esto puede ser posible porque, durante la ejecución de un programa de Java, la JVM convierte el código objeto en código binario correspondiente a la arquitectura donde se esté ejecutando. Cabe mencionar que debe existir
CAPÍTULO 3: INTRODUCCIÓN AL LENGUAJE JAVA 17
una JVM apropiada para cada arquitectura en donde se desee ejecutar código de Java. Actualmente, existen JVMs para Solaris, SunOs, Windows 9x, Windows NT, Linux, HP-UX, Irix, Aix, MacOS, Apple, entre otros sistemas operativos.
3.2.6 Multitarea Permite la ejecución concurrente de varios procesos ligeros o hilos de ejecución (mejor rendimiento), esto es, Java permite realizar muchas actividades simultáneas en un programa. El beneficio de ser multitarea (multi-threaded ) consiste en un mejor rendimiento interactivo y mejor comportamiento en tiempo real que los entornos de flujo único de programa ( single-threaded ).
3.3 Programación básica en Java Esta capítulo presenta la sintaxis para implementar en Java ciertos aspectos que todo lenguaje de programación de alto nivel debe considerar. Gran parte de esta sintaxis ha sido heredada de C/C++.
3.3.1 Comentarios Los comentarios se utilizan para documentar el código, esto es, proporcionan una explicación general de lo que algún segmento de código realiza o, tal vez, información adicional que no se lee directamente en el código. Los comentarios deben contener sólamente información que sea relevante para la lectura y entendimiento del programa. En java, existen 3 tipos de comentarios: 1) Comentarios de una sóla línea. Cualquier caracter que se encuentre en la misma línea y después de los caracteres // será considerado comentario y, por lo tanto, ignorado por el compilador: x = x + 1;
// esta línea incrementa en uno el valor de x
2) Comentarios de una o más líneas. Cualquier carácter que se encuentre entre las parejas de caracteres /* y */ serán considerados comentarios e ignorados por el compilador: x = x + 1; /* dentro de este ciclo, el valor de x será incrementado en uno mientras éste sea menor a 100 */ y = /* esto es válido, pero no recomendable */ y + 1;
3) Comentarios de documentación. Estos comentarios son exclusivos de Java y están delimitados por los caracteres /** y */. También son de una o más líneas. Los comentarios de documentación describen la especificación que ha dado lugar al código desde una perspectiva más general que los comentarios de implementación (los 2 anteriores), para que pueda ser comprendida por los desarrolladores que no les interese comprender el código fuente, sino las funciones que proporciona tal código. Utilizando la herramienta javadoc podemos extraer estos comentarios a ficheros HTML [véase Capítulo 12.4]. /** El siguiente método devuelve el valor del área de un rectángulo de base b y altura h; el área es un valor flotante */ public float getArea(float b, float h) { …
18 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
3.3.2 Identificadores Los identificadores sirven para nombrar unívocamente a variables, funciones, clases y objetos definidos por el usuario. Un identificador debe comenzar con una letra, un subrayado o un símbolo de peso. Los caracteres siguientes pueden ser letras, dígitos o subrayados. Identificadores válidos: x, _variable , nombre_cliente , Nombre_cliente , $abc, MiClase3
3.3.3 Tipos de datos y literales literales Los tipos de datos definen los métodos de almacenamiento disponibles para representar información, junto con la manera en que dicha información ha de ser interpretada. Java es un lenguaje con control fuerte de tipos (Strongly Typed ), ), esto significa que cada variable y cada expresión tiene asignado un tipo de dato que es conocido en el momento de la compilación. Este tipo limita los valores que una variable puede contener, limita las operaciones soportadas sobre esos valores y determina el significado de las operaciones. Los tipos de datos en Java pueden dividirse en dos categorías: simples y compuestos . Los simples son tipos atómicos que no se derivan de otros tipos: enteros, reales, lógicos y caracteres. Los tipos compuestos se construyen con base en los tipos simples: cadenas de caracteres, arreglos, clases e interfaces. A excepción de las cadenas de caracteres, los tipos de datos compuestos son tipos definidos por el usuario o por el programador de librerías de Java. En esta capítulo veremos los tipos de datos simples y las cadenas de caracteres. Cada tipo de dato simple soporta un conjunto de literales, las cuales definen la sintaxis que debe tener un valor, esto es, la forma de expresar/representar valores constantes como 134, ‘a’, -75.6.
3.3.3.1 Enteros Se utilizan para representar números enteros con signo. Hay cuatro tipos: byte, short, int y long.
Tipo
Tamaño
byte
1 Byte (8 bits)
short
2 Bytes (16 bits)
int
4 Bytes (32 bits)
long
8 Bytes (64 bits) Tabla 1 Tipos de datos enteros
Literales enteros; o o o
Decimal: Los literales decimales aparecen como números ordinarios sin ninguna notación especial: 155, -3456 Hexadecimal: Los hexadecimales (base 16) aparecen con un 0x ó 0X inicial, notación similar a la utilizada en C y C++: 0x3F, 0xAD16 Octal: Los octales aparecen con un 0 inicial delante de los dígitos: 017, 032345
CAPÍTULO 3: INTRODUCCIÓN AL LENGUAJE JAVA 19
Por ejemplo, un literal entero para el número decimal 10 se representa en Java como 10 en decimal, como 0xA en hexadecimal, y como 012 en octal. Los literales enteros se almacenan por defecto en el tipo int , o si se trabaja con números muy grandes, en el tipo long, añadiendo una L ó l al final del número.
3.3.3.2 Reales Se usan para representar números con partes fraccionarias. Hay dos tipos de datos reales en Java: float y double. El primero reserva almacenamiento para un número de precisión simple de 4 bytes y el segundo lo hace para un numero de precisión doble de 8 bytes. Tipo
Tamaño
float
4 Byte (32 bits)
double
8 Bytes (64 bits)
Tabla 2 Tipos de datos reales
Literales reales: Representan números decimales con partes fraccionarias. Pueden representarse con notación estándar (156.75) o científica (3.2456e2). De forma predeterminada, un valor real es de tipo double (8 bytes). Para tratar un valor real como float se agrega el caracter F ó f al final del número: 3.1492f.
3.3.3.3 Lógicos Se usan para almacenar variables que presenten dos estados: verdadero ó falso. Representan valores biestado, provenientes del álgebra de Boole. Hay un tipo de dato lógico en Java: boolean. Literales lógicos: Java utiliza dos palabras clave para los estados: true (para verdadero) y false (para falso). Este tipo de literales es nuevo respecto a C/C++, donde el valor de falso se representaba por un 0 numérico, y el verdadero con cualquier número diferente a 0.
3.3.3.4 Caracter Utilizados para almacenar caracteres Unicode simples. Hay un tipo de dato caracter en Java: char. Cabe mencionar que Unicode es una especificación que proporciona un número único para cada caracter existente, sin importar la plataforma, el programa o el idioma. Debido a que el conjunto de caracteres Unicode se compone de valores de 16 bits, el tipo de datos char se almacena en un entero sin signo de 16 bits. Literales caracter: Representan un único carácter (de la tabla de caracteres Unicode 1.1) y aparecen dentro de un par de comillas simples, de forma similar que en C/C++: ‘a’, ‘3’, ‘#’. Los caracteres especiales (de control, no
20 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
desplegables) se representan con una barra invertida ('\') seguida del código asociado, como se puede observar en la Tabla 3. Descripción
Representación
Caracter Unicode
Valor Unicode
\udddd
Numero octal
\ddd
Retroceso
\b
\u0008
Retorno de carro
\r
\u000D
Tabulación horizontal
\t
\u0009
Línea nueva
\n
\u000A
Barra invertida (\)
\\
\u005C
Comillas simples (’)
\’
\u0027
Comillas dobles (”)
\"
\u0022
Números arábigos ASCII
‘0’ – ‘9’
\u0030 a \u0039
Alfabeto ASCII en mayúsculas
‘A’ – ‘Z’
\u0041 a \u005A
Alfabeto ASCII en minúsculas
‘a’ – ‘z’
\u0061 a \u007A
Tabla 3 Caracteres especiales Java
3.3.3.5 Cadenas de caracteres Utilizados para almacenar información textual (letras, palabras) de tamaño indefinido. En Java se utiliza el tipo de dato String para almacenar cadenas de caracteres. A diferencia de C/C++, el tipo de dato String es una clase de Java, y una variable de tipo String es un objeto [véase Capítulo 5] y, por lo mismo, tiene atributos y proporciona métodos útiles para la manipulación de cadenas de texto. Literales para cadenas de caracteres: Una cadena de caracteres representa múltiples caracteres los cuales se encuentrar limitados por un par de comillas dobles (“”): “ésta es una cadena de caracteres”. Aunque una variable String realmente sea un objeto, Java nos permite tratar a este tipo de variables como si pertenecieran a un tipo de dato primitivo, esto es, podemos asignarle valores constantes directamente sin preocuparnos por reservar espacio de memoria previamente, como se tiene que hacer para los demás objetos.
3.3.4 Operadores Java proporciona un conjunto de operadores para poder realizar acciones sobre uno o dos operandos. Un operador que actúa sobre un sólo operando es un operador unario , y un operador que actúa sobre dos operandos es un operador binario. Existen operadores (!, -) que pueden funcionar como ambos. Los operadores de Java se pueden dividir en cinco principales categorías: aritméticos, relacionales, condicionales, a nivel de bits y de asignación.
CAPÍTULO 3: INTRODUCCIÓN AL LENGUAJE JAVA 21
3.3.4.1 Aritméticos Estos operadores pueden actuar sobre números enteros y reales; en algunos casos, también actúan sobre cadenas de texto. Los operadores binarios aritméticos soportados por Java son: + – * / % +
: : : : : :
suma los operandos; resta el operando de la derecha al de la izquierda multiplica los operandos divide el operando de la izquierda entre el de la derecha residuo de la división del operando izquierdo entre el derecho concatena cadenas de texto
Los operadores unarios aritméticos soportados por Java son: + – ++ ––
: : : :
indica un valor positivo negativo, o cambia el signo albegraico suma 1 al operando, como prefijo o sufijo resta 1 al operando, como prefijo o sufijo
3.3.4.2 Relacionales Estos operadores son útiles para comparar valores numéricos, lógicos o caracteres. Devuelven siempre un valor lógico (true, false). Todos los operadores relacionales son binarios. > >= < <= == ¡=
: : : : : :
el operando izquierdo es mayor que el derecho el operando izquierdo es mayor o igual que el derecho el operando izquierdo es menor que el derecho el operando izquierdo es menor o igual que el derecho el operando izquierdo es igual que el derecho el operando izquierdo es diferente al derecho
3.3.4.3 Condicionales (lógicos) Estos operadores sirven para realizar operaciones utilizando valores lógicos. Todos los operadores condicionales son binarios y devuelven un valor lógico ( true, false). && : operador AND; devuelve true si ambos operandos son true | | : operador OR; devuelve true si algún operando es true ! : operador NOT; alterna el valor lógico del operando
3.3.4.4 A nivel de bits Java y C/C++ comparten un conjunto de operadores que realizan operaciones sobre un sólo bit a la vez. Además, Java incluye el operador >>> que no existe en C/C++, el cual nos permite realizar desplazamientos a la derecha ignorando el bit de signo, esto es, rellena con ceros los bits más
22 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
significativos que pueden quedar vacíos. Los operadores a nivel de bits actúan sobre números enteros únicamente y se listan a continuación: Operadores binarios: >>
: operador SHR; desplaza bits del operando izquierdo hacia la derecha las posiciones indicadas en el operando derecho; este operador SI considera el bit de signo << : operador SHL; desplaza bits del operando izquierdo hacia la izquierda las posiciones indicadas en el operando izquierdo; >>> : operador SHR; desplaza bits del operando izquierdo hacia la derecha las posiciones indicadas en el operando derecho; este operador NO considera el bit de signo & : operador AND | : operador OR ^ : operador XOR Operadores unarios: ~
: complemento del operando (NOT)
3.3.4.1 De asignación =
: operador binario de asignación de valores; el valor representado por el operando de la derecha del operador es copiado en la memoria indicada por el operando de la izquierda
Java incluye todo una gama de operadores de asignación que se componen de otros operadores para realizar la operación que indique ese operador y luego asignar el valor obtenido al operando de la izquierda del operador de asignación. De este modo se pueden realizar dos operaciones con un solo operador: += , –=, *=, /=, %=, &=, |=, ^=, <<=, >>=, >>>=
3.3.5 Separadores Los separadores sirven para definir la forma en que se va a agrupar y va a funcionar el código. Los separadores admitidos en Java son: ( ) – paréntesis. Para contener listas de parámetros en la definición y llamada a métodos. También se utiliza para definir precedencia en expresiones, contener expresiones para control de flujo ( for , while) y rodear las conversiones de tipo (casts) [véase Capítulo 3.6]. { } – llaves . Para contener los valores de arreglos inicializados en la declaración. También se utiliza para definir los límites de clases, subclases, métodos y bucles. [ ] – corchetes . Para declarar, instanciar y manipular valores de arreglos [véase Capítulo 7.1]. ; – punto y coma. Para separar líneas de código (sentencias). , – coma. Para separar identificadores consecutivos en una declaración de variables. También se utiliza para separar declaraciones, incrementos/decrementos y condiciones finales en sentencias for .
CAPÍTULO 3: INTRODUCCIÓN AL LENGUAJE JAVA 23
. – punto. Para separar nombres de paquetes, subpaquetes y clases. También se utiliza para especificar un atributo o método público a partir del identificador del objeto [véase Capítulo 5.2].
3.4 Impresión en pantalla En ocasiones es importante conocer el valor de alguna variable en algún momento de la ejecución de la aplicación; Java proporciona dos métodos útiles para imprimir información en la consola. sintaxis: System.out.print(expresion); System.out.println(expresion); // imprime un retorno de carro al final (enter)
donde expresion puede ser: a) una variable primitiva: x, aa, y b) una constante : 2, ‘x’, ‘\n’, 0xF370, -15.6, “ja”, PI c) un objeto: cadenaTexto, miArreglo, vector1, triangulo3 d) una operación : x * y, (base + altura) / 2, s + “hola” La clase final System tiene un atributo estático llamado out de tipo PrintStream (flujo de impresión); la clase PrintStream proporciona los métodos print y println, los cuales se pueden invocar utilizando como parámetro un valor de cualquier tipo de dato. Esto significa que podemos imprimir cualquier cosa en la consola –enteros, reales, lógicos, caracteres, cadenas, objetos– ya que, finalmente, Java convierte el valor a imprimir en una cadena de texto. Este valor puede ser el resultado de alguna operación. ejemplos: System.out.print(58.75); System.out.print(area / 2); System.out.println(“hola”); System.out.println(“area del círculo es ” + area);
El signo + puede cambiar su significado de acuerdo a los operadores; el signo + representa una: • suma aritmética : si todos los operadores son valores numéricos • concatenación : si al menos un operador es una cadena de texto ejemplos: int a = 5; System.out.print(100 + a + 25); System.out.print(100 + a + “25”);
// imprimirá 130 // imprimirá 10525
Podemos utilizar paréntesis para indicar la operación que se realizará primero en una expresión. ejemplos: String s = “5”; int a = 2; System.out.print(s + a + 10);
// imprimirá 5210 porque al menos uno // de los operadores es una cadena de texto
System.out.print(s + (a + 10));
// imprimirá 512 porque primero realiza // la suma aritmética de enteros y luego // la concatenación de cadenas
System.out.print(s + a * 10);
// imprimirá 520 porque primero realiza // la multiplicación aritmética y luego // la concatenación de cadenas
24 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
3.5 Control de flujo Las sentencias de control de flujo nos permiten crear flujos de programa alternativos que se tomarán de acuerdo a las acciones del usuario de la aplicación y, en general, al valor de las variables en algún momento de la ejecución. Cada flujo de programa indica un camino diferente que conlleva a la terminación del programa. Muchas de estas sentencias que veremos a continuación se han tomado de C/C++. Existen 3 tipos principales de sentencias de control de flujo: a) Sentencias de salto : para elegir un proceso a ejecutar dada una condición b) Sentencias de bucle : para repetir un proceso mientras no se cumpla una condición c) Excepciones : para manejar errores en tiempo de ejecución
3.5.1 Sentencias de salto Las sentencias de salto eligen la siguiente secuencia de operaciones a ejecutarse, entre varias posibles, de acuerdo al cumplimiento de alguna condición. Java proporciona 2 tipos de sentencias de salto que hereda de C/C++: if y switch.
3.5.1.1 Sentencia if La sentencia if analiza el cumplimiento de una condición dada a través de una variable o expresión lógica; esto es, si el valor de la variable lógica o el resultado de la expresión resulta verdadero se ejecuta una secuencia de operaciones; en caso contrario, se ejecuta otra secuencia de operaciones, pero esta ejecución es opcional. El efecto de la sentencia if consiste en elegir una ruta a seguir de solamente 2 caminos posibles. La Figura 20 muestra un diagrama de flujo con estructura de árbol binario que representa el comportamiento de la sentencia if . ? true
false
Figura 20 Diagrama de flujo de la sentencia if
sintaxis: if( expresionLogica ) { sentencias; // condición cumplida; expresionLogica = true } else { más sentencias; // condición no cumplida; expresionLogica = false }
NOTAS: a) la presencia de la sentencia else es opcional: existen casos en los que no se necesita tomar algún camino cuando la condición no se cumple b) se puede prescindir de las llaves si el camino a seguir consta de una sóla sentencia ejemplo: public void operacion(float a, float b, int opcion) { // si la opcion no esta entre 1, 2 salir del metodo; ejemplo de if(opcion <= 0 || opcion > 2) return;
if
sin
else
CAPÍTULO 3: INTRODUCCIÓN AL LENGUAJE JAVA 25 if(opcion == 1) { float suma = a + b; System.out.println(“a + b = ” + suma); } else { float resta = a - b; System.out.println(“a - b = ” + resta); } }
En el método anterior solamente podemos realizar una de dos operaciones aritméticas, de acuerdo al valor de la variable opcion . Si queremos incrementar la funcionalidad de este método podemos utilizar sentencias if anidadas. La figura 21 muestra un diagrama de flujo con estructura de árbol binario que representa el comportamiento de la sentencia if anidada. ? true
false
? true
false
Figura 21 Diagrama de flujo de la sentencia if anidada
sintaxis: if( expresionLogica1 ) { sentencias; // expresionLogica1 = true } else if { expresionLogica2 ) { sentencias; // expresionLogica2 = true; expresionLogica1 = false; } : : else if ( expresionLogican ) { sentencias; // expresionLogican = true; todas las demas = false; } else { sentencias; // todas las expresiones = false; }
ejemplo: public void operacion(float a, float b, int opcion) { float total = 0.0f; if(opcion <= 0 || opcion > 4) { System.out.println(“opcion no valida”); return; } // no es necesario agregar un else en la siguiente línea por la presencia de // la sentencia return if(opcion == 1) total = a + b; else if(opcion == 2) total = a – b; else if(opcion == 3) total = a * b; else { // es necesario prevenir una división entre cero if(b == 0) System.out.println(“divisor no válido”); else total = a / b; } System.out.println(“resultado de la operación: ” + total); }
26 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
3.5.1.2 Sentencia switch La sentencia switch analiza una variable de tipo entero o caracter y, dado el valor obtenido, se elegirá uno de varios caminos posibles. Esta sentencia suele ser una alternativa ideal para evitar el anidamiento de sentencias if . La Figura 22 muestra un diagrama de flujo con estructura de árbol n-ario que representa el comportamiento de la sentencia switch. ?
...
Figura 22 Diagrama de flujo de la sentencia switch
sintaxis: switch(variable) { case valor1: sentencias; break; case valor2: sentencias; break; : default: sentencias; }
NOTAS: a) variable debe ser de algún tipo entero o caracter (int, short, byte, char) b) valor1, valor2 son constantes del mismo tipo que variable c) la presencia de default es opcional (como else en la sentencia if ) d) break evita que se ejecutan todos los casos ejemplo: public void operacion(float a, float b, int opcion) { float total = 0.0f; switch(opcion) { case 1: total = a + b; case 2: total = a – b; case 3: total = a * b; case 4: if(b == 0) System.out.println(“divisor no válido”); else total = a / b; default: System.out.println(“opcion no valida”); } System.out.println(“resultado de la operación: ” + total); }
3.5.2 Sentencias de bucle Las sentencias de bucle nos sirven para repetir una secuencia de instrucciones tantas veces como sea necesario, esto es, mientras no se cumpla la condición de terminación. Java proporciona 3 tipos de sentencias de bucle –o ciclos– que también son heredadas de C/C++: for, while, do/while.
3.5.2.1 Ciclo for Este ciclo es uno de los más usados para repetir una secuencia de instrucciones sobre todo cuando se conoce la cantidad exacta de veces que se desea ejecutar un proceso dado.
CAPÍTULO 3: INTRODUCCIÓN AL LENGUAJE JAVA 27 sintaxis: for (inicializacion; condicion_terminacion; inc/decremento) { sentencias; }
La inicialización es una instrucción de asignación que carga una o más variables de control de ciclo con sus valores iniciales. Se pueden declarar tales variables dentro del ciclo for . La condición de terminación es una expresión relacional que evalúa la variable de control de ciclo contra un valor final o de parada que determina cuando debe acabar el ciclo. El incremento o decremento define la manera en que la variable de control de ciclo debe cambiar cada vez que finalice un ciclo. Se deben separar esos 3 argumentos con punto y coma (;). ejemplos: for(int i = 0; i < 5; i ++) System.out.print(i); // imprime 01234 int m = 0, n = 100; for (int i = 0, j = 10; i < 10 ; i ++, j --) { m += i; n -= j; } // al final m, n = 45
3.5.2.2 Ciclo while En este ciclo, el cuerpo de instrucciones se ejecutará mientras una condición dada permanezca como verdadera; en el momento en que la condición se convierte en falsa, el ciclo terminará; nótese que es posible que el cuerpo de instrucciones no se ejecute nunca. sintaxis: declarar/iniciar variables condicionales while (condicion) { sentencias; instruccion(es) que nos permitiran salir del ciclo en algun momento }
donde condicion es una expresión relacional, y las instrucciones para salir del ciclo tarde o temprano serán capaces de cambiar el valor actual de condicion . ejemplo: public int obtenerFactorialDe(int numero) { int factorial = 1; int i = 2; while(i < numero) { factorial = factorial * i; i ++; } return factorial; }
3.5.2.3 Ciclo do / while Su diferencia básica con el ciclo while es que la prueba de condición se realiza al finalizar el ciclo; con esto, el cuerpo de instrucciones se ejecutará al menos una vez; por esta razón se le conoce también como ciclo de condición de salida.
28 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA sintaxis: declarar/iniciar variables condicionales do { sentencias; instrucción(es) para salir del ciclo } while (condicion);
donde condicion es una expresión relacional, y las instrucciones para salir del ciclo tarde o temprano serán capaces de cambiar el valor actual de condicion . ejemplo: public void fibonacci(int limite) { int a = 0; int b = 1; do { int c = a + b; System.out.println(c); a = b; b = c; } while(a + b < limite); }
3.6 Conversión de tipos Java realiza una conversión automática de tipos numéricos cuando el tipo de dato de la variable resultante es mayor que el tipo del valor original. Esto es, cuando deseamos convertir un valor de byte a short , short a int , float a double , char a int , o de un tipo entero a un tipo real, el tipo de dato resultante está implícito en la asignación. Si el tipo de dato de la variable resultante es menor que el tipo del valor original, es necesario realizar un moldeo o cast al valor original para forzar la conversión; se dice que el tipo de dato resultante está explícito en la asignación. El cast se realiza colocando entre parentésis el tipo de dato al que se quiere convertir un valor. sintaxis : tipoDato1 identificador = (tipoDato1) valor
donde valor tiene un tipo mayor que tipoDato1 ejemplo : short sh = (short) 10000;
Existen otros tipos de conversiones de tipos que involucran cadenas de caracteres; en estos casos, se utilizan métodos de ciertas clases provistas por la API de Java: Integer, String, Float, entre otros. A continuación, se proporcionan ejemplos de conversiones de tipos comunes en Java y se clasifican de acuerdo a los tipos de datos participantes en la conversión. Entre tipos enteros: byte a short/int: byte b = 0x3F; short s = b; int i = b;
short a int: short s = 300; int i = s;
CAPÍTULO 3: INTRODUCCIÓN AL LENGUAJE JAVA 29 int a short/byte: short s = (short) 1000; byte b = (byte) 0x7F; byte b = (byte) s;
Entre tipos reales: float a double: float f = 1567.3; double d = f;
double a float: float float
f = (float) 56789.34; f = 56789.34f;
Entre tipos enteros y reales: float a int: int i = (int) 1505.67f;
// i = 1505
double a long: long l = (long) -1335345.45;
// l = -1335345
int a float: float f = 7501;
// f = 7501.0f
long a double: double d = 56780000;
// d = 5678000.0
Entre tipos numéricos y cadenas de texto: int a String: String st = new Integer(1500).toString() ;
// st = “1500”
float a String: String st = new Float(1500.5f).toString( );
// st = “1500.0”
String a int: int i = new Integer("1500").intValue (); ó int i = Integer.parseInt(“1500”);
// i = 1500
String a float: float f = new Float(“1500.0f”).floatValu e(); ó float f = Float.parseFloat(“1500.0f”);
// f = 1500.0f
int a binario: String st = Integer.toBinaryString(0 x0F34);
// st = “0000111100110100”
Entre tipos carácter y enteros: int a char: char c = (char) 65;
// c = ‘A’
char a int: int i = ‘A’;
// i = 65
30 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
Entre tipos carácter y cadenas de texto: char a String: String st = String.valueOf(‘A’);
// st = “A”
String a char: String st = “hola”; char c = st.charAt(0);
// c = ‘h’
CAPÍTULO 4
ESTRUCTURA DE UN PROGRAMA
A continuación se presenta la estructura general de un programa de Java: // encabezado de la clase package geometria; import java.util.Vector; import estadisticas.*; // definición de la clase principal public class Triangulo { // seccion de declaración de atributos private float base, height; // seccion de implementación de métodos public float getArea() { return (base * height) / 2; } }
4.1 Encabezado El encabezado de una clase es lo primero que se escribe en un programa de Java, pero su presencia no es obligatoria. En el encabezado especificamos el paquete al que pertenece la clase actual, y todas las librerías de clases que va a utilizar en algún lugar del código. Estas librerías pueden ser de Java o propias del programador. La instrucción package nos permite agrupar clases e interfaces. Java organiza sus librerías de clases en paquetes de forma similar a C++. Los nombres de los paquetes son palabras separadas por puntos y se almacenan en directorios que coinciden con esos nombres. De esta manera, a cada directorio de clases de Java -o del programador- le corresponde un nombre de paquete. sea paquete de la forma : nombreDirectorio(.nombreSubdirectorio)* donde (*) indica que podemos incluir 0 o más subdirectorios separados por (.) sintaxis: package paquete package paquete; ;
32 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA ejemplo:
El archivo Vector.java contiene el código fuente de una clase de Java que está incluido originalmente en el subdirectorio \java\util\ de la API; por lo tanto, el encabezado de esta clase debe contener la línea de código: package java.util;
La instrucción import nos permite utilizar clases que no estén en nuestro directorio de trabajo o en nuestro proyecto; estas clases incluyen librerías de Java o librerías propias; el similar de C++ es la directiva #include . sintaxis: import paquete import paquete.nombreClase; .nombreClase; import paquete import paquete.*; .*;
Retomando el ejemplo anterior, todas las clases que deseen utilizar la clase Vector deberán incluir la siguiente línea de código: import java.util.Vector;
o si se desea utilizar más de una clase que se encuentre en un directorio: import javax.swing.*;
Es importante saber que esta sentencia sólamente incluye las clases que se encuentren en el directorio \javax\swing\, lo que significa que no incluye las clases contenidas en los subdirectorios; para incluirlas hay que especificar el nombre del subdirectorio. En el siguiente ejemplo se importan todas las clases contenidas en los subdirectorios \event y \border. import javax.swing.event.*; import javax.swing.border.*;
Una clase que incluya estas 2 líneas en su encabezado podrá utilizar todas las clases contenidas en los paquetes javax.swing.event y javax.swing.border.
4.2 Definición de la clase Todo archivo que contenga código Java deberá tener asociado un nombre único de acuerdo al paquete (directorio) al que pertenezca, respetando la sintaxis definida en la Sección 3.3.2. La extensión de tal archivo debéra ser .java. Además, deberá contener una sóla clase principal con el mismo nombre del archivo, la cual deberá ser pública y podrá contener atributos y métodos. Dentro de una clase también se pueden definir subclases con sus propios atributos y métodos; a esto se le denomina anidamiento de clases . En capítulos posteriores se presentarán programas de ejemplo que incluyen clases anidadas. Lo más recomendable es que el programador defina un archivo diferente por cada clase que vaya a desarrollar. sintaxis: public class NombreClase { // cuerpo del programa }
CAPÍTULO 4: ESTRUCTURA DE UN PROGRAMA 33 ejemplo: package miPaquete.geometria; import miPaquete.estadisticas.*; public class Triangulo { … }
Esta clase deberá guardarse en el archivo: \miPaquete\geometria\Triangulo.java , y puede utilizar todas las clases contenidas en el directorio: \miPaquete\estadisticas\ .
4.3 Declaración de atributos Los atributos son las variables globales de una clase que pueden ser llamadas dentro de cualquier método de la misma o por otras clases (si son protegidas o públicas). Un atributo puede ser una variable de tipo primitivo (int, float) o un objeto (su tipo es una clase). sintaxis: [visibilidad] [modificador] tipo_dato identificador [= valor_inicial] •
•
visibilidad [véase Sección 2.3] o public para atributos públicos o protected para atributos protegidos o private para atributos privados o por omisión de visibilidad, el atributo es público dentro de las clases del mismo paquete; este nivel de visibilidad es denominado Friendly o amigable en C/C++ modificador : cambia el comportamiento de un atributo [véase Capítulo 6.1] o final para atributos constantes o static para atributos estáticos
•
tipo_dato : es el tipo de dato del atributo (int, float, String, NombreClase … )
•
identificador : nombre de la variable
•
valor_inicial : el valor predeterminado de una variable; si el atributo es un objeto el valor inicial puede ser null o puede ser una instanciación [véase Capítulo 5.1].
ejemplos: public String s = “esto es una cadena pública”; protected float base, altura, area; private MiClase m = null;
Si dos o más atributos comparten el mismo tipo de dato y los mismos modificadores, pueden declararse en una sóla línea de código separados por comas, como se observa en el segundo ejemplo.
4.4 Declaración de métodos Los métodos de una clase son funciones que pueden ser invocadas dentro de cualquier método de la misma clase o por otras clases (si no son privados).
34 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA sintaxis: [visibilidad] [modificador] tipo_devuelto identificador(tipo1 id1, tipo2 id2, …, tipon idn) donde n • 0 •
•
•
•
•
visibilidad [véase Capítulo anterior] modificador: cambia el comportamiento de un método [véase Capítulo 6.2] o static para métodos estáticos o abstract para métodos abstractos o synchronized para métodos sincronizados tipo_devuelto : es el tipo de dato que devuelve el método; void si no devuelve nada; el tipo devuelto puede ser primitivo o una clase identificador: nombre del método tipo1 id1, tipo2 id2, …, tipo n idn : lista de los parámetros de entrada; cada parámetro incluye el tipo de dato y el nombre de la variable; el tipo de dato puede ser primitivo o una clase
ejemplos: public float getArea(float height, float base) public static String intToBinary(int i) protected void removeRange(int fromIndex, int toIndex) private Shape3D getShape3D(int index)
Cabe mencionar que no pueden existir 2 métodos con el mismo nombre y los mismos parámetros de entrada dentro de una clase. Cuando tenemos 2 métodos con el mismo nombre y tipo devuelto, pero con diferentes parámetros de entrada se dice que realizamos sobrecarga de funciones .
4.4.1 Invocación de métodos y sentencia return Si el tipo devuelto por el método es void , entonces el método no devuelve ningún valor, y la sentencia return nos servirá para abandonar el método en cualquier momento sintaxis: return;
ejemplo: public void iniciarTriangulo(float b, float a) { if(a == 0 || b == 0) return; // si algún parametro es 0, abandonar el método base = b; altura = a; }
Para invocar un método cuyo tipo devuelto es void , especificamos el nombre del método y enseguida los parámetros de entrada entre paréntesis. Si el método no tiene parámetros de entrada, los paréntesis no se eliminan en la invocación. ejemplos: inciarTriangulo(10.0f, 7.5f); borrarTriangulo();
CAPÍTULO 4: ESTRUCTURA DE UN PROGRAMA 35
Si el tipo devuelto por el método no es void, entonces el método tiene que devolver un valor cuyo tipo de dato sea igual al tipo devuelto, en caso contrario, se genera un error de sintaxis. Ese valor será recibido por el proceso que invocó al método. sintaxis: return expresion;
donde expresion puede ser: a) una variable primitiva: x, aa, y b) un objeto: cadenaTexto, miArreglo, vector1, triangulo3 c) una constante : 0, null, -15.6, “ja”, PI, new Circulo(15) [véase Capítulo 5.1] d) una operación : x * y, (base * altura) / 2, s + “hola” ejemplo: public float obtenerCociente(float numerador, float denominador) { if(denominador == 0.0f) return 0.0f; float cociente = numerador / denominador; return cociente; // otra solucion: return numerador / denominador; }
Para invocar un método que devuelve un tipo diferente a void utilizamos la misma sintaxis que para un método que devuelve void , con la excepción de que aquí lo tratamos si fuera un valor cuyo tipo está dado por el tipo devuelto. ejemplos: promedio = obtenerMayor(x, 5, 7); // donde x, promedio ya están declaradas float cociente = obtenerCociente(10.0f, 0.0f); // cociente = 0.0f float prom = (obtenerCociente(10.0f, 2.0f) + obtenerCociente(10.0f, 5.0f))/2; // prom = (5.0f + 2.0f) / 2 = 3.5f
lo siguiente no está bien, porque un valor no puede estar aislado: obtenerCociente(12.84f, 8.56f);
es como si escribiéramos: 1.5f;
Cabe mencionar que debe existir una sentencia return para cada ruta posible que nos lleve a la terminación del método. Esto es muy frecuente cuando hacemos uso de las sentencias de salto y de bucle [véase Sección 3.5]. ejemplo: public int obtenerMayor(int a, int b, int c) { if(a >= b) if(a >= c) return a; else return c; else if(b >= c) return b; else return c; }
4.4.2 Constructores El primer método que se ejecuta al crear un objeto es el constructor de la clase al que pertenece este objeto. El constructor nos permite realizar algún proceso de inicialización antes de que se comiencen a utilizar los métodos de la clase. El constructor debe ser público, no devuelve ningún valor y su nombre deberá ser igual al de la clase al que pertenece. Una clase puede tener 0 ó más constructores, donde cada uno deberá tener una lista diferente de parámetros de entrada.
36 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA sintaxis: public NombreClase(/* parametros de entrada */) { …
Cuando definimos más de un constructor en una clase (con diferentes parámetros de entrada) se dice que realizamos sobrecarga de constructores . ejemplo: public class Triangle { private float base, height; public Triangle(float b, float h) { base = b; // en este height = h; // inicial } public Triangle(float b) { base = b; // en este height = 2 * h; // está en } public setBase(float b) { base = b; } public setHeight(float h) { height = h; } public float getArea() { return (base * height) / 2; } }
constructor, se especifican de manera los parámetros del triángulo constructor, el valor de la altura función de la base
4.4.3 Método main Una aplicación de Java consta de una o más clases, pero una de ellas será necesariamente la puerta de entrada para la ejecución de la aplicación, esto es, al menos una clase deberá incluir el método principal de la aplicación a partir del cual se crean todas las demás clases; al igual que C/C++ y otros lenguajes de programación, Java consta con un método principal (main) el cual se invocará automáticamente al momento de ejecutar una clase con el comando c:\>java NombreClase, o a través de algúna función de un IDE (Integrated Development Environment) de Java. sintaxis: public static void main(String[] args) { …
Como en C/C++, el nombre del método principal de Java es main y devuelve void , además es público, estático [véase Sección 6.2] y espera un arreglo de cadenas de texto. Este arreglo representa la lista de argumentos de la aplicación. A diferencia de C/C++, este método no espera la longitud del arreglo (int argc) ya que todo arreglo en Java tiene un atributo length útil para obtener la cantidad de elementos [véase Sección 7.1.3]. ejemplo: public class UnaClase { final PI = 3.1416;
// clase que imprime el area de un circulo // de radio 1.5
public static void main(String[] args) { float radio = 1.5f; float area = PI * Math.pow(radio, 2); // pow es un metodo estático de la clase // Math para efectuar la potenciación System.out.println(“El radio es: ” + area); } }
CAPÍTULO 4: ESTRUCTURA DE UN PROGRAMA 37 ejecución: c:\>java UnaClase El radio es: 7.0686 c:\>
Cabe mencionar que una clase que solamente tenga el método main pierde las características de la orientación a objetos y se comporta como cualquier programa estructurado de C/C++.
4.5 Ámbito de una variable El ámbito de una variable representa el contexto dentro del cual una variable está definida y puede ser utilizada. Las variables de Java sólo son válidas desde el punto donde están declaradas hasta el final de la sentencia compuesta que la engloba. Se pueden anidar estas sentencias compuestas, y cada una puede contener su propio conjunto de declaraciones de variables locales. Sin embargo, no se puede declarar una variable con el mismo nombre que una de ámbito exterior. El siguiente ejemplo intenta declarar dos variables separadas con el mismo nombre. En C/C++ son distintas, porque están declaradas dentro de ámbitos diferentes; sin embargo, esto es ilegal en Java. ejemplo: public void imprimeDobles(int max) { int j = 0; for(int i = 0; i < max; i ++) { int j = 2 * i; System.out.println(j); } }
// crea un ámbito // crea un nuevo ámbito // error de compilación
Las variables se puede declarar en tres lugares diferentes: 1. En el encabezado de la clase: pueden ser utilizados por todos los métodos de la clase; pueden ser públicos, privados o protegidos 2. Dentro de un método: son reconocidos dentro del método donde se declaró; desaparecen cuando se abandona el método 3. En un bucle (if/else, switch, for, while, do/while): son reconocidos sólamente dentro del bucle; dejan de existir al terminar el bucle ejemplo: public class Estadisticas { private int limite; public Estadisticas(int limite) { this.limite = limite; } public int obtenerSumatoria( ) { int sum = 0; for(int i = 0; i < limite; i ++) { sum += i; } return sum; } }
Variables de clase: limite Variables de metodo: limite, sum Variables de bucle: i
// ambito 1 inicia // ambito 2 inicia // ambito 2 termina // ambito 3 inicia // ambito 4 inicia // ambito 4 termina // ambito 3 termina // ambito 1 termina
38 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
Cabe mencionar que Java permite declarar variables de método o de bucle con el mismo nombre que las variables de clase; en ese caso, cuando se realiza un llamado a tales variables, se estará refiriendo a las variables de método/bucle. Para poder distinguir las variables de clase (atributos) de las variables de método/bucle se utiliza la instrucción this seguida de un punto y el nombre del atributo, como se puede apreciar en el ejemplo anterior.
4.6 Traducción de una clase en notación UML a una clase en código Java En El Capítulo 2.2 se presentó la notación UML para representar gráficamente una clase, pero no se incluyeron tipos de datos de los atributos ni los tipos devueltos de los métodos. La sintaxis para especificar tales situaciones es la siguiente: * nombreAtributo : tipoDato * nombreMétodo(tipo1 id1, …, tipo n idn) : tipoDevuelto donde * ∈ { +, #, -} NOTA: si tipoDevuelto = void entonces no se especifica en el modelo
A continuación se presentan dos ejemplos prácticos de codificación de una clase de Java a partir de su modelo en notación UML. ColorRGB + R, G, B : int + ColorRGB(int r, int g, int b) + ColorRGB( )
Cuadrado – lado : float # posX, posY: int + Cuadrado(float l) + Cuadrado(int x, int y, float l) + mover(int dx, int dy) + obtenerLado() : float + redimensionar(int dl)
public class ColorRGB { public int R, G, B; public ColorRGB(int r, int g, int b) { R = r; // color Rojo G = g; // color Verde B = b; // color Azul } public ColorRGB() { R = 127; // crear un color gris G = 127; // de manera predeterminada B = 127; } }
public class Cuadrado { private float lado; protected int posX, posY; public Cuadrado(float l) { lado = l; } public Cuadrado(int x, int y, float l) { posX = x; posY = y; lado = l; } public void mover(int dx, int dy) { posX += dx; posY += dy; } public float obtenerLado() { return lado; } public void redimensionar(int dl) { lado += l; } }
CAPÍTULO 5
OBJETOS
Para comprender la relación existente entre un objeto y una clase, analicemos la siguiente frase: Un plano es a un conjunto de casas, como una clase es a un conjunto de objetos
Así como un plano es un modelo que describe la manera en que se va a construir una casa, una clase es una plantila que describe la manera en que se va a comportar un objeto; todas las casas que se construyan a partir de un mismo plano deberán exhibir un conjunto de características visuales y funcionales en común. Similarmente, todos los objetos creados a partir de una clase compartirán un conjunto de atributos y métodos que definirán su comportamiento. A partir de un plano podemos crear muchas casas y a partir de una clase podemos crear muchos objetos.
5.1 Creación Una clase por sí sola no tiene vida, es decir, no puede ejecutarse; para poder ejecutar una clase y utilizar sus atributos y métodos es necesario crear una instancia de esa clase: un objeto. Un objeto, en cambio, sí tiene vida: se crea, se utiliza, evoluciona y, tarde o temprano, es destruido (por el programador o por el recolector de basura). Cuando creamos un objeto, estamos reservando un espacio de memoria en donde se almacenará el estado del objeto: los valores de todos sus atributos y de las variables del método en ejecución. Una variable cuyo tipo sea un objeto es realmente una referencia o apuntador a tal espacio de memoria. Para crear un objeto –instanciar una clase– utilizamos la sentencia new de la siguiente manera: sintaxis: Clase1 objeto1 = new Clase1(/* parámetros */); // crea un objeto y lo asigna // a una variable // crea un objeto sólamente new Clase1(/* parámetros */);
ejemplos: ColorRGB color1 = new ColorRGB(190, 128, 50); Cuadrado cuadrado1 = new Cuadrado(x, y, 3.5f); new Aplicacion();
también podemos crear objetos en la invocación de métodos o con la sentencia return :
40 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA ejemplos: String s = invertirTexto(new String(“mensaje a invertir”)); cambiarFigura(indice, new Cuadrado(x, y, lado)); return new Triangulo(base, altura);
5.2 Uso de atributos y métodos de clase Un programa en ejecución es una colección de objetos, donde dichos objetos interactuantes son creados y destruídos. Esta interacción se basa en mensajes que son mandados de un objeto a otro, donde el emisor le pide al receptor que aplique un método a sí mismo. Es entonces a partir de los objetos que podemos manipular los atributos e invocar los métodos de una clase, siempre y cuando éstos sean declarados como públicos. Para realizar ello especificamos el del objeto y el nombre del atributo/método público a utilizar separados por un punto. sintaxis : objeto.atributo = 15; objeto.metodo(/* parámetros */); variable = objeto.metodo(/* parámetros */);
// metodo que devuelve void // metodo que devuelve algo
ejemplos: ColorRGB color1 = new ColorRGB(); color1.R = 156; color1.B = color.G - 10; System.out.println(“color es: ” + color1.R + “,” + color1.G + “,” + color1.B); // imprime: color es: 156, 127, 117 Cuadrado c = new Cuadrado(10, 20, 15.5f); c.mover(2, -3); // atributos posX y posY valen 12 y 17, respectivamente c.redimensionar(1.5f); float lado = c.obtenerLado(); // lado = 17.0f; c.redimensionar(c.obtenerLado()); System.out.println(c.obten erLado()); // imprime 34.0f;
5.3 Pase por valor / referencia El pase por valor se efectúa si, al invocar un método, las variables que se incluyeron en los parámetros no modificarán su valor al terminar el método. Internamente, la aplicación realiza una copia de la variable en una nueva localidad de memoria. En el pase por referencia, por lo contrario, una variable enviada como parámetro sí podrá modificar su valor al terminar el método; esto se debe a que el método recibe realmente la localidad de memoria donde se encuentra dicha variable. En C/C++ el pase por referencia se realiza utilizando apuntadores. Como Java maneja no apuntadores, de manera predeterminada el pase por valor en Java se realiza con variables cuyo tipo de dato sea primitivo (int, char, float) o String , y el pase por referencia se lleva a cabo utilizando objetos, esto es, variables cuyo tipo de dato sea una clase. ejemplo: public class Pase { public void aumentarRojo(ColorRGB color, int r) { r += 15; color.R += 15; } public Pase() { int r = 50; ColorRGB color = new ColorRGB(r, 150, 200); // color pasa por referencia, r por valor aumentarRojo(color, r);
CAPÍTULO 5: OBJETOS 41 System.out.println(color.R) ; System.out.println(r);
// imprime 65 (objeto modificado) // imprime 50 (variable intacta)
} public static void main(String[] args) { new Pase(); } }
Lo mismo sucede en la igualación de variables: si las variables son de tipo primitivo, se pasa el valor del dato origen a la variable destino, pero las variables son independientes entre sí. En cambio, si las variables son objetos, se pasa la referencia del dato origen a la variable destino, y cualquier cambio que suceda en el objeto fuente se verá reflejado en el destino, y viceversa. ejemplo: ColorRGB c1 = new ColorRGB(100, 200, 150); ColorRGB c2 = c1; System.out.println(c2.R); // imprime 100 c1.R = 40; System.out.println(c2.R); // imprime 40
5.4 Alcance de un objeto Los objetos de Java tienen un tiempo de vida y consumen recursos durante el mismo; cuando un objeto ya no se va a usar debería liberar el espacio que ocupaba en la memoria de forma que las aplicaciones no la agoten. En Java, la liberación de memoria es responsabilidad de un proceso llamado recolector automático de basura o garbage collector , en inglés. El recolector de basura monitorea el alcance de los objetos creados y elimina aquellos que ya se salieron del alcance. Un objeto se dice que se sale del alcance cuando ya no existe una referencia hacia él, es decir, no existe una variable que apunta al espacio de memoria asignado para tal objeto. Normalmente, este alcance se pierde cuando se crea un nuevo objeto x utilizando una variable que apuntaba previamente a un objeto y; en este caso, se dice que y queda fuera del alcance. ejemplo: String s; s = new String(“hola”); s = “adiós”;
// // // //
memoria no asignada, objeto no creado memoria asignada, objeto creado nueva memoria asignada, nuevo objeto creado con la misma variable
Al final de la tercera sentencia, el primer objeto creado de nombre s que contiene la cadena “hola” se ha salido del alcance porque ya no hay forma de acceder a él; como consecuencia de ello, este objeto es marcado y eliminado posteriormente por el recolector de basura. Una forma de liberar inmediatamente un objeto es asignándole un valor null. ejemplo: ColorRGB color = new ColorRGB(100, 200, 150); … color = null; // se perdió el alcance al objeto; en este momento // el recolector de basura deberá marcar el objeto // para eliminarlo posteriormente
CAPÍTULO 6
MODIFICADORES
Los modificadores son palabras reservadas de Java que se colocan delante de la definición de los atributos o métodos de una clase con el fin de proporcionarles una característica adicional. Un modificador define la forma en que serán utilizados los atributos y métodos. En capítulos anteriores se han descrito los modificadores de acceso, private, protected y public, las cuales nos permiten limitar el acceso a los componentes de una clase o a la clase en si. A continuación se presentan los principales modificadores que proporciona Java, y se clasifican de acuerdo al tipo de elemento que califican: atributo, método o clase.
6.1 Para atributos final: un atributo final nunca podrá cambiar su valor durante la ejecución de la clase que la contiene; se utiliza para declarar constantes. sintaxis: [visibilidad] final identificador = valor;
ejemplo: public final double PI = 3.1416;
no se podrá hacer esto: }
public void iniciarPI() { PI = 3.141592;
static: un atributo estático es una variable que no se asocia a una instancia de una clase (objeto), sino que se asocia a la clase misma; no hay una copia del dato para cada objeto, sino una sóla copia que es compartida por todos los objetos pertenecientes a la clase; si uno de los objetos cambia el valor del atributo, éste cambiará para todos los objetos. sintaxis: [visibilidad] static tipoDato identificador [= valor_inicial];
44 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA ejemplo: public class Punto { int posX , posY; static int numPuntos = 0; Punto(int posX, int posY) { this.posX = posX; this.posY = posY; numPuntos ++ ; System.out.print(numPuntos + “ ”); }
Punto p1 = new Punto p2 = new Punto p3 = new // el programa // 1 2 3
Punto(1,-3); Punto(3, 10); Punto(12, 6); imprimira:
}
Un atributo puede ser public, static y final al mismo tiempo, lo que indica que tal atributo será una constante compartida entre muchas clases; además, podremos tener acceso a tal atributo sin realizar una instancia de la clase que la contiene. Un ejemplo típico es el atributo PI de la clase Math: public final class Math { public static final double PI = 3.1516926535; … … }
modo de uso: float diametro = 2 * radio * Math.PI; Math.PI += 2; // incorrecto
6.2 Para métodos static: al igual que con los atributos, un método estático no es propia de una instancia de una clase, sino de la clase misma; lo anterior implica que podemos utilizar un método estático (público) sin crear una instancia de la clase que la contiene, sino especificando sólamente el nombre de su clase. Un método estático sólamente accede a variables estáticas. Cualquier variable declarada dentro de un método estático será también estática. sintaxis: [visibilidad] static tipoDevuelto nombreMetodo( /* parametros */) { …
ejemplos: public class Punto { int posX , posY; static int numPuntos = 0; Punto(int posX, int posY) { this.posX = posX; this.posY = posY; numPuntos ++ ; } public static int obtenerPuntos() { return numPuntos; }
Punto p1 = new Punto(1,-3); Punto p2 = new Punto(3, 10); Punto p3 = new Punto(12, 6); int p = p3.obtenerPuntos(); // p = 3
} public class Stats { public static float obtenerMayor(float a, float b) { if(a >= b) return a; else return b; } }
CAPÍTULO 6: MODIFICADORES
45
modo de uso: float m = Stats.obtenerMayor(3.5, 100); // m = 100
abstract: un método abstracto es aquel que no se implementa, es decir, nada más se especifica su prototipo en la clase; los métodos abstractos pertenecen exclusivamente a clases abstractas. Más detalles sobre métodos y clases abstractas en la Sección 8.3. sintaxis: [visibilidad] abstract tipoDevuelto nombreMetodo( /* parametros */);
synchronized: un método sincronizado sólo puede ser invocado por un proceso a la vez; definir métodos sincronizados nos permite gestionar la concurrencia. Cuando se invoca un objeto sincronizado, el objeto que lo contiene se bloquea hasta que finalice el método. Si existen muchos métodos sincronizados, sólo un método puede estar activo en un objeto a la vez; todos los demás procesos que intentan invocar métodos sincronizados deberán esperar. Estos métodos son útiles sólamente cuando se desarrollan aplicaciones multihilos [véase Capítulo 10]. sintaxis: [visibilidad] synchronized tipoDevuelto nombreMetodo( /* parametros */) { …
6.3 Para clases abstract: una clase abstracta no puede ser instanciada, sólamente derivada; las clases abstractas nos sirven para definir un comportamiento en común de algún conjunto de clases (las subclases); una clase abstracta puede contener métodos abstractos, los cuales deberán ser implementados por todas sus subclases. Para más detalles sobre clases abstractas, consulte la Sección 8.3.1. sintaxis: public abstract class NombreClase { …
final: una clase final no podrá tener clases derivadas o subclases; las clases finales cierran la cadena de la herencia [véase Capítulo 8]. Una clase declarada como final se considera como una clase completa, la cual no es necesaria especializarla para agregarle funcionalidad. Ejemplos clásicos de clases finales son: String y Math. Algunas de las razones por las que se declaran clases finales son: desempeño, diseño del sistema, seguridad e integridad de objetos. Cabe mencionar que una clase no puede ser final y abstract al mismo tiempo. sintaxis: public final class NombreClase { …
lo siguiente no será permitido: public final class NombreClase1 extends NombreClase { …
En cuanto al tema de seguridad, un mecanismo que utilizan mucho los hackers para alterar sistemas es crear una subclase de una clase perteneciente a una aplicación, y luego sustituir la clase original por su clase. La subclase del hacker tendrá la misma apariencia y será tratada por Java de la misma manera que la clase original, pero hará cosas diferentes, como causar daño u obtener información privada.
46 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
Como ejemplo de alteración de clases, a continuación se presenta una clase que hereda de la clase java.util.Vector; esta clase, también llamada Vector, sobreescribe el método size() de tal manera que devuelve el doble del tamaño real del vector. Para más detalles del uso de vectores y la sintaxis de la herencia de clases, véanse las Secciones 7.2 y 8.2, respectivamente. public class Vector extends java.util.Vector { public Vector() { super(); // inicializa la clase padre } public int size() { // sobrecarga del método size() return 2 * super.size(); // obtiene el tamaño del vector y lo duplica } }
una aplicación que utilice la clase Vector:
Vector v = new Vector(); v.addElement(new String("elemento 1")); v.addElement(new String("elemento 2")); System.out.println(v.size( ));
// // // //
se creó un vector se agregó un elemento se agregó otro elemento imprime 4, en lugar de 2
6.3.1 Clases no derivables / no instanciables Una forma alternativa y muy útil para especificar que una clase no se puede derivar es definiendo un constructor privado. El compilador de Java no permitirá extender tal clase porque el método constructor no es alcanzable desde fuera de su clase. El uso de esta técnica tiene como consecuencia que no podamos crear instancias de una clase fuera de ella. El esquema adoptado por la siguiente clase es útil para evitar que se realice más de una instancia de una clase: public class Singular { static private Singular singular = new Singular(); private Singular() { System.out.println("creado"); // algun proceso de inicialiación } public static Singular crearSingular() { return singular; } }
modo de uso: Singular sin1 = Singular.crearSingular() ; Singular sin2 = Singular.crearSingular() ; Singular.crearSingular(); // al final de la ejecución se verá impreso
// sin1 y sin2 representan 2 // variables para el mismo objeto // otra forma de invocar el método “creado” una sóla vez (no 3)
Es para considerar que si definimos un constructor protegido, la clase que lo contiene únicamente podrá ser instanciada dentro de las clases derivadas.
6.4 Representación de modificadores utilizando notación UML Para simplificar la representación de modificadores dentro del modelo de una clase, se adoptarán las siguientes convenciones que representan una extensión de la notación UML.
CAPÍTULO 6: MODIFICADORES
Atributo final: NEGRITA Atributo estático: subrayado Atributo final estático: NEGRITA-SUBRAYADO Método abstracto: cursiva ( …) Método estático: subrayado ( … ) Método sincronizado: sync nombreMetodo ( ... ) Clase final: Negrita Clase abstracta: Cursiva ejemplo:
Console – text: String – readLine ( ) + getf ( ) : float + geti ( ) : int + gets ( ) : String
clase correspondiente: public final class Console { private static String text = ""; private static void readLine() { … } public static float getf() { … } public static int geti() { … } public static String gets() { … } }
// línea leída desde la consola // lee una línea de la consola y lo // asigna al atributo ‘text’ // convierte la línea leída en flotante // y lo devuelve // convierte la línea leída en entero // y lo devuelve // devuelve la línea leída
47
CAPÍTULO 7
ALMACENAMIENTO DE DATOS
En este capítulo se estudian 3 técnicas muy comunes provistas por Java para el almacenamiento de colecciones de datos en memoria: arreglos, vectores y tablas hash.
7.1 Arreglos Un arreglo es una sucesión ordenada y finita de datos del mismo tipo. En Java, podemos declarar arreglos de cualquier tipo primitivo e incluso de objetos. De manera similar a las cadenas de caracteres, no se puede conocer la longitud que tendrá un arreglo en el momento de su declaración; por esta razón, Java trata a los arreglos como objetos que necesitan instanciarse antes de ser utilizados, y liberarse al concluir su uso.
7.1.1 Declaracíón Para declarar un arreglo en Java, especificamos el tipo de elementos que va a almacenar, seguido del identificador del arreglo y una pareja de corchetes para especificar que estamos declarando un arreglo. Al igual que en otros lenguajes de programación, también podemos declarar arreglos de dos o más dimensiones. En estos casos agregamos una pareja de corchetes por cada dimensión adicional. sintaxis: tipoDato identificador[]; ó tipoDato[] identificador;
// arreglo de una dimensión // arreglo de una dimensión
tipoDato identificador[]…[]; // arreglo de más de una dimensión ó tipoDato[]…[] identificador; // arreglo de más de una dimensión
ejemplo: String listaClaves[]; Triangulo triangulos[]; float coordenadas2D[][]; double[][][] coordenadas3D;
7.1.2 Instanciación Una vez declarado los arreglos, se debe proceder a la instanciación (creación) para reservar un espacio de memoria dentro del cual se almacenarán los elementos del arreglo. Si utilizamos el arreglo sin crear previamente la instancia encontraremos errores en tiempo de ejecución. Debido a que un arreglo es
50 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
tratado como un objeto, para instanciarlo utilizamos la instrucción new seguida del tipo de dato que almacena el arreglo y el(los) tamaño(s) especificado(s) entre corchetes. Una vez definida la longitud del arreglo, ésta no podrá cambiar durante la ejecución del programa; la única forma de redefinirla es volviendo a instanciar el arreglo, lo que ocasionará la pérdida de datos. sintaxis: identificador = new tipoDato[tamaño]; // arreglo de una dimensión identificador = new tipoDato[tamaño 1]…[tamaño n]; // arreglo de n dimensiones
donde: tamaño es una variable o constante de tipo entero y mayor que cero. ejemplos : listaClaves triangulos coordenadas2D coordenadas3D
= = = =
new new new new
String[35]; Triangulo[max]; float[25][80]; double[100][100][100];
También podemos declarar y crear el arreglo en una sóla línea de código, por ejemplo, en la sección de declaración de atributos de la clase. sintaxis : tipodato identificador[] = new tipoDato[tamaño]; tipodato identificador[]…[] = new tipoDato[tamaño 1]…[tamaño n]
ejemplos : String listaClaves[] Triangulo[] triangulos float coordenadas2D[][] double[][][] coordenadas3D
= = = =
new new new new
String[35]; Triangulo[max]; float[25][80]; double[100][100][100];
7.1.3 Manipulación Para leer/modificar un elemento de un arreglo –previamente declarado y creado– se especifica el nombre del arreglo y entre corchetes el índice donde se encuentra el elemento a manipular. sintaxis : valor = nombreArreglo[indice]; valor = nombreArreglo[indice1][indice2]; nombreArreglo[indice] = nuevoValor; nombreArreglo[indice1][indice2] = nuevoValor;
Si el arreglo almacena objetos, entonces se tiene que instanciar los elementos del arreglo antes de ser utilizados. sintaxis : nombreArreglo[indice] = new tipodato(/* parametros */); nombreArreglo[indice1][indice2] = new tipodato(/* parametros */);
Para obtener la longitud de un arreglo se utiliza el atributo length accesible desde cualquier arreglo: sintaxis : int longitud1 = nombreArreglo.length; // longitud de la 1ra dimensión int longitud2 = nombreArreglo[0].length; // longitud de la 2da dimensión
CAPÍTULO 7: ALMACENAMIENTO DE DATOS 51 ejemplo: public float obtenerMayor(float[] numeros) { if(numeros == null) return 0; // si el arreglo no ha sido instanciado float max = numeros[0]; for(int i = 1; i < numeros.length; i ++) if(numeros[i] > max) max = numeros[i]; return max; }
7.1.4 Ejemplo A continuación se presenta la clase Lienzo la cual contiene un arreglo que almacena objetos de tipo Rectangulo; la clase Rectangulo está definida dentro de la clase Lienzo, y su constructor recibe la base y la altura del rectángulo. Las líneas de código en negrita realizan algún tipo de operación con arreglos. El constructor de Lienzo espera el número de rectángulos que se agregarán utilizando el método agregarRectángulo (). El método obtenerMayor () devuelve el rectángulo con mayor área. public class Lienzo { private Rectangulo[] recta ngulos; private int numRectanguloActual = 0; class Rectangulo { // definición de una clase anidada public float base, altura; public Rectangulo(float base, float altura) { if(base > 0) this.base = base; else this.base = 1.0f; if(altura > 0) this.altura = altura; else this.altura = 1.0f; } public float obtenerArea() { return base * altura / 2; } } public Lienzo (int no_rectangulos) { if(no_rectangulos <= 0) no_rectangulos = 1; rectangulos = new Rectangulo[no_rectangulos]; } public void agregarRectangulo(float base, float altura) { if(numRectanguloActual == rectangulos.length) return; rectangulos[numRectanguloActual] = new Rectangulo(base, altura); numRectanguloActual ++; } public Rectangulo obtenerMayor() { if(rectangulos == null) return null; // si el arreglo no se ha creado Rectangulo maxTemp = rectangulos[0]; for(int i = 1; i < rectangulos.length; i ++) { float area = rectangulos[i].obtenerArea(); if(area > maxTemp.obtenerArea()) maxTemp = rectangulos[i]; } return maxTemp; } public static void main(String[] args) { Lienzo l = new Lienzo(3); l.agregarRectangulo(3.5f, 4.4f); l.agregarRectangulo(12.1f, -2.5f); l.agregarRectangulo(10.0f, 2.6f); Rectangulo recMayor = l.obtenerMayor(); System.out.println("Area mayor = " + recMayor.obtenerArea()); System.out.println("Rectangulo: " + recMayor.base + ", " + recMayor.altura); } // imprime: Area mayor = 13.0 } // Rectangulo: 10,0, 2.6
52 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
Nótese que Lienzo trabaja igual para cualquier número de rectángulos, pero tal número se deberá especificar inicialmente y no se podrá cambiar a menos que se cree otro objeto de tipo Lienzo. Además, la aplicación lanzará un error en tiempo de ejecución en caso de que se agreguen menos rectángulos del máximo. Si se agregan más rectángulos del máximo, serán ignorados por la clase Lienzo. Esta técnica no se recomienda para manejar estructuras de datos de tamaño dinámico.
7.1.5 Declaración / instanciación / rellenado Existe una forma para declarar, crear y rellenar un arreglo con una sóla línea de código. Esta solución es útil cuando sabemos de antemano cuántos y cuáles son los elementos que va a contener un arreglo. sintaxis : tipodato nombreArreglo[] = { valorInicial1, …, valorInicialn };
donde el tipo de dato de valorInicial debe ser tipodato ejemplos :
int numeros[] = { 15, -34, 100, 0x3F, 0}; System.out.println(numeros [2]); // imprime 100 Rectangulo r = new Rectangulo(4, 2.5f); Rectangulo[] rect = { new Rectangulo(10, 20), r, null }; System.out.println(rect[1] .obtenerArea()); // imprime 5
Nota: no olvidar que el valor null puede ser asignado a cualquier objeto, sin importar a qué clase pertenezca.
7.1.6 Igualación Recordando que los arreglos en Java son objetos, la igualación de arreglos tiene el mismo efecto que la igualación de objetos, esto es, se copia la referencia al primer elemento del arreglo, y no se copian los elementos en sí. Si hacemos un cambio en el arreglo destino, éste se va a ver reflejado en el arreglo fuente, y viceversa. ejemplo : float[] arreglo1 = { 1.5f, 3.4f, -12.75f, 36.0f }; float[] arreglo2; // igualación de referencias arreglo2 = arreglo1; arreglo2[3] -= 5; // cambio en arreglo destino se verá System.out.println(arreglo 1[3]); // reflejado en el fuente; imprime 31.0
Si se desea copiar el contenido solamente, y mantener la independencia entre los arreglos, es necesario crear un nuevo arreglo y copiar uno por uno cada elemento. ejemplo : float[] arreglo1 = { 1.5f, 3.4f, -12.75f, 36.0f }; float[] arreglo2 = new float[4]; for(int i = 0; i < arreglo1.length; i ++) // igualación de valores arreglo2[i] = arreglo1[i]; arreglo2[3] -= 5; // cambio en arreglo destino no se verá System.out.println(arreglo 1[3]); // reflejado en el fuente; imprime 36.0
CAPÍTULO 7: ALMACENAMIENTO DE DATOS 53
7.2 Vectores Para solucionar el problema inherente de los arreglos, el tamaño fijo, Java proporciona la clase Vector, la cual nos permite almacenar muchos datos en una estructura de datos de tamaño variable. Un vector es una sucesión ordenada de elementos de diferente tipo. Cada elemento deberá ser un objeto, debido a que los vectores no admiten variables primitivas. El tamaño del vector es 0 inicialmente, pero podrá cambiar dinámicamente conforme a las adiciones y eliminaciones realizadas.
7.2.1 Declaración / creación La clase Vector forma parte del paquete java.util; por lo tanto, es necesario importar tal paquete en todas las clases en donde se vayan a utilizar vectores. Es muy común que declararemos e instanciemos un vector en una sóla línea de código, ya que el tamaño del vector no se especifica en la creación. sintaxis: import java.util. Vector; … Vector nombreVector = new Vector();
7.2.2 Adición de elementos Para agregar elementos utilizamos el método miembro de la clase Vector: addElement( ); este método recibe como parámetro un objeto, el cual se agregará al final del vector; el tamaño del vector se incrementará en uno automáticamente. sintaxis: nombreVector.addElement(nombreObjeto); nombreVector.addElement(new NombreClase(/* parametros */ ));
Si se desea guardar un elemento en una posición dada del vector y sustituir el elemento actual, se utiliza el método setElementAt( ) de la siguiente manera: sintaxis: nombreVector.setElementAt(nombreObjeto, indice);
donde: indice es un entero mayor o igual a 0 y menor al tamaño actual del vector.
7.2.3 Navegación La navegación de un vector involucra búsqueda, obtención de elementos y conocimiento del tamaño del vector. Al igual que en un arreglo, la búsqueda en un vector es secuencial; podemos referirnos a un elemento específico a través del índice, esto es, el lugar relativo que ocupa dentro del vector. Para obtener un elemento de un vector utilizamos el método get ( ), el cual recibe como parámetro la posición que ocupa tal elemento dentro del vector y devuelve un objeto de tipo Object; para tratar el objeto devuelto como un objeto del tipo esperado es necesario convertirlo a través de un cast . sintaxis: Clase objetoObtenido = (Clase) nombreVector.get(indice);
donde: indice es un entero mayor o igual a 0 y menor al tamaño actual del vector. Clase es el tipo de dato esperado del objeto situado en la posición indice
54 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
Para obtener el tamaño actual del vector utilizamos el método size( ) de la siguiente manera: sintaxis : int tamaño = nombreVector.size();
7.2.4 Eliminación de elementos Para eliminar elementos de un vector, podemos utilizar el método miembro: removeElementAt( ); este método recibe como parámetro la posición del objeto que queremos eliminar. El tamaño del vector se decrementará en uno automáticamente. sintaxis : nombreVector.removeElementAt(indice);
donde: indice es un entero mayor o igual a 0 y menor al tamaño actual del vector. Si se desea obtener la referencia del objeto que se desea eliminar del vector para realizar alguna operación con él antes de ser eliminado, utilizamos el método remove( ) de la siguiente manera: sintaxis : Clase objetoEliminado = (Clase) nombreVector.remove(indice);
donde: indice es un entero mayor o igual a 0 y menor al tamaño actual del vector. Clase es el tipo de dato esperado del objeto a eliminar Como el método remove ( ) devuelve un objeto de tipo Object, es necesario efectuar un cast al objeto devuelto para convertirlo en un objeto del tipo esperado.
7.2.5 Ejemplo A continuación se implementa la clase Lienzo, presentada en El Capítulo 7.1.4, utilizando vectores; esta nueva versión va a permitir que se agreguen rectángulos de manera indefinida; para ejercitar los métodos vistos en las capítulos anteriores, la clase va a permitir obtener, eliminar, sustituir y desplegar los datos de un rectángulo, además de obtener el rectángulo con el área mayor. Las líneas de código en negrita realizan alguna operación con vectores. import java.util.Vector; public class Lienzo { private Vector rectangulos = new Vector(); class Rectangulo { public float base, altura;
// definición de una clase anidada
public Rectangulo(float base, float altura) { if(base > 0) this.base = base; else this.base = 1.0f; if(altura > 0) this.altura = altura; else this.altura = 1.0f; } public float obtenerArea() { return base * altura / 2; } } public void agregarRectangulo(float base, float altura) { rectangulos.addElement(new Rectangulo(base, altura)); }
CAPÍTULO 7: ALMACENAMIENTO DE DATOS 55 public Rectangulo obtenerRectangulo(int index) { if(index < 0 || index >= rectangulos.size()) return null; Rectangulo r = (Rectangulo) rectangulos.get(index); return r; } public void eliminarRectangulo(int index) { if(index < 0 || index >= rectangulos.size()) return; rectangulos.removeElementAt(index); } public void sustituirRectangulo(int index, Rectangulo r) { if(index < 0 || index >= rectangulos.size()) return; rectangulos.setElementAt(r, index); } public void desplegarRectangulo(Rectangulo r) { System.out.println(“Base = ” + r.base + “. Altura = ” + r.altura); System.out.println(“Area = ” + r.obtenerArea()); } public int obtenerMayor() { if(rectangulos.size() < 0) return -1; int indiceMax = 0; float maxTmp = obtenerRectangulo(0).obtenerArea(); for(int i = 1; i < rectangulos.size(); i ++) { float area = obtenerRectangulo(i).obtenerArea(); if(area > maxTmp) { maxTmp = area; indiceMax = i; } } return indiceMax; } public static void main(String[] args) { Lienzo1 l = new Lienzo1(); l.agregarRectangulo(3.5f, 4.4f); l.agregarRectangulo(12.1f, -2.5f); l.agregarRectangulo(10.0f, 2.6f); int indiceMayor = l.obtenerMayor(); Rectangulo r = l.obtenerRectangulo(indiceMayor); l.desplegarRectangulo(r); } // la aplicación imprime: Base = 10.0 Altura = 2.6 } // Area = 13.0
7.3 Tablas hash Una tabla hash es una colección no ordenada y de tamaño variable de objetos de diferente tipo. La característica más importante de las tablas hash es que proporcionan acceso directo a cada elemento, en lugar del típico acceso secuencial inherente en los arreglos y vectores. El acceso a un elemento de la tabla se realiza a través de su clave asignada en lugar de utilizar su posición relativa (índice); esto surge para solucionar el problema de desempeño implícito en los largos ciclos de búsqueda de elementos que cumplan cierta condición. Al igual que con los vectores, los elementos de las tablas hash no pueden ser de tipo primitivo. El tamaño inicial de la tabla es 0, pero podrá cambiar dinámicamente conforme a las adiciones y eliminaciones realizadas.
7.3.1 Declaración / creación Java proporciona la clase Hashtable que nos sirve para implementar tablas hash; esta clase también forma parte del paquete java.util, por lo tanto, es necesario importar tal paquete en todas las clases en
56 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
donde se vayan a utilizar tablas hash. Es muy común que declararemos e instanciemos una tabla hash en una sóla línea de código, ya que su tamaño no se especifica en la creación. sintaxis : import java.util.Hashtable; … Hashtable nombreTabla = new Hashtable();
7.3.2 Adición de elementos Cada elemento deberá tener asociado una clave única en el momento de ser agregado a la tabla. Al igual que los elementos, las claves son objetos; se recomienda utilizar claves de tipo String, Integer o Float ya que podemos realizar igualaciones entre objetos de esos tipos, utilizando el método miembro: equals ( ). Para agregar elementos utilizamos el método miembro de la clase Hashtable: put ( ); este método recibe como parámetro el elemento a agregar y el valor de la clave que tendrá asignado; el tamaño de la tabla se incrementará en uno automáticamente. sintaxis : put(clave, elemento); nombreTabla.
donde: clave y elemento son objetos ejemplo :
miTabla. put(new Integer(105), new Rectangulo(10.0f, 30.0f));
7.3.3 Obtención de elementos Para obtener un elemento de una tabla hash utilizamos el método miembro get ( ); este método recibe como parámetro la clave del elemento a buscar y devuelve un objeto de tipo Object; para tratar el objeto devuelto como un objeto del tipo esperado es necesario convertirlo a través de un cast . sintaxis : Clase objetoObtenido = (Clase) nombreTabla.get(clave);
donde: clave es un objeto Clase es el tipo de dato esperado del objeto que tiene asignado clave ejemplo : Rectangulo r = (Rectangulo) miTabla.get(new Integer(105));
7.2.4 Eliminación de elementos Para eliminar elementos de una tabla hash, se utiliza el método miembro: remove( ); este método recibe como parámetro la clave asignada al objeto que queremos eliminar. El tamaño de la tabla hash se decrementará en uno automáticamente. sintaxis : nombreTabla.remove(clave);
donde: clave es un objeto
CAPÍTULO 7: ALMACENAMIENTO DE DATOS 57 ejemplo: tablaAlumnos.remove(new String(“190”));
Este método también nos permite obtener la referencia del objeto que se desea eliminar para realizar alguna operación con él antes de ser eliminado. sintaxis: Clase objetoEliminado = (Clase) nombreTabla.remove(clave);
donde: clave es un objeto Clase es el tipo de dato esperado del objeto que tiene asignado clave ejemplo: Alumno a = (Alumno) tablaAlumnos.remove(new String(“190”));
7.2.5 Ejemplo A continuación se presenta la clase Inventario, la cual contiene una tabla hash que almacena objetos de tipo Producto ; un producto tiene asignado una clave alfanumérica a través de la cual podrá ser identificada, además del nombre, el precio y las existencias. Para ejercitar los métodos vistos en las capítulos anteriores, la clase va a permitir obtener, agregar, eliminar, modificar y desplegar los datos de un producto. Las líneas de código en negrita realizan alguna operación con tablas hash. import java.util.Hashtable; public class Inventario { private Hashtable productos = new Hashtable(); private Producto obtenerProducto(String clave) { Producto p = (Producto) productos.get(clave); if(p == null) Imprimir.mensaje(0, clave); return p; } public void agregarProducto(String c, String n, float f, int e) { productos.put(c, new Producto(c, n, f, e)); Imprimir.mensaje(1, c); } public void eliminarProducto(String clave) { if( productos.remove(clave) == null) Imprimir.mensaje(0, clave); else Imprimir.mensaje(2, clave); } public void cambiarPrecio(String clave, float precio) { Producto p = obtenerProducto(clave); if(p != null) { p.cambiarPrecio(precio); Imprimir.mensaje(3, clave); } } public void desplegarProducto(String clave) { Producto p = obtenerProducto(clave); if(p != null) p.desplegar(); } }
Para comprender mejor la operación de la clase Inventario, a continuación se lista el código de las clases Impresión y Producto, utilizadas en Inventario. La comprensión de las líneas de código de cada una de estas clases se deja al lector.
58 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA public class Producto { private String clave; private String nombre; private float precio; private int existencias; public Producto(String clave, String nombre, float precio, int existencias) { this.clave = clave; this.nombre = nombre; this.precio = precio; this.existencias = existencias; } public String claveEs() { return clave; } public void cambiarPrecio(float precio) { if(precio > 0) this.precio = precio; } public void desplegar() { Imprimir.linea('-', 30); System.out.println(" Clave: " + clave); System.out.println(" Nombre: " + nombre); System.out.println(" Precio: " + precio); System.out.println("Existencias: " + existencias); Imprimir.linea('=', 30); } } public final class Imprimir { public static void mensaje(int no_mensaje, String clave) { switch(no_mensaje) { case 0: System.out.println("Clave " + clave + " no valida"); break; case 1: System.out.println("Producto " + clave + " agregado con exito"); break; case 2: System.out.println("Producto " + clave + " eliminado con exito"); break; case 3: System.out.println("Precio del producto " + clave + " cambiado con exito"); } } public static void linea(char caracter, int repeticion) { for(int i = 0; i < repeticion; i ++) System.out.print(caracter); System.out.println(); } }
La clase Hashtable proporciona el método elements ( ) que nos devuelve una sucesión ordenada de todos los objetos almacenados en la tabla. El tipo de dato de esta sucesión es Enumeration, una interfaz que proporciona dos métodos útiles para realizar recorridos secuenciales y obtener los elementos de la sucesión: • •
hasMoreElements ( ) : devuelve true si la sucesión tiene más elementos false si llegó al final de la sucesión nextElement ( ) : devuelve el objeto siguiente; cast necesario
El siguiente método se agregará a la clase Inventario de tal manera que se pueda imprimir un catálogo de todos los productos dados de alta. public void desplegarTodos() { Enumeration e = productos.elements(); while(e.hasMoreElements()) {
CAPÍTULO 7: ALMACENAMIENTO DE DATOS 59 Producto p = (Producto) e.nextElement(); p.desplegar(); } }
Otra forma de resolver el problema del recorrido en tablas hash es utilizando estructuras de datos secuenciales indexados. Uniendo las capacidades de los vectores y las tablas hash, podemos designar un vector para almacenar las claves de todos los productos y una tabla hash para almacenar la información de cada producto. Cada vez que se agreguen o eliminen productos de la tabla, se tendrá que hacer una operación similar en el vector. Cuando se busque un producto para edición o consulta, no será necesaria la participación del vector. En cambio, cuando se desee realizar recorridos (barridos) del inventario de productos, se tendrán que efectuar búsquedas secuenciales a lo largo del vector, y a través de cada clave obtenida se buscará el producto respectivo en la tabla hash. De esta manera, combinamos las facilidades para realizar accesos directos en tablas hash con los recorridos secuenciales en vectores. Se deja al lector modificar la clase Inventario de tal manera que utilice la técnica secuencial indexado.
7.4 Implementación de la asociación de clases a) Relación 1..*, 1..+: Sea A la clase con multiplicidad 1 y B la clase con multiplicidad * ó + en una relación de asociación. A tendrá un atributo designado como clave y representado en el modelo de manera explícita con un asterisco. B tendrá un atributo de manera implícita cuyo tipo de dato será el mismo que el de la clave de A. El nombre y la visibilidad de este atributo estarán dados por la etiqueta contenida en la relación del lado de B, como se puede apreciar en el esquema de la Figura 23. De manera predeterminada todo atributo clave es privado . modelo: ClaseA
1
* – id_A
ClaseB ...
* idClase : tipoDato …
Figura 23. Esquema de la relación de asociación 1..*
implementación: public class ClaseA { private tipoDato idClase; … } public class ClaseB { private tipoDato id_A ; … }
A través del atributo id_A, la clase B podrá hacer referencia al objeto de tipo A que tendrá asociado. ejemplo: Cliente * no_cliente : int …
1
* + cliente
Factura
60 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA implementación: public class Factura { public int cliente; // este atributo almacenara la clave del cliente … // al que se le emite la factura }
7.5 Implementación de la agregación de clases a) Relación 1..1: La clase Agregación tendrá un atributo implícito cuyo tipo de dato es la clase Componente . El nombre y la visibilidad de tal atributo estará dado mediante una etiqueta situada en la relación del lado del componente, como se puede apreciar en la Figura 24. La instanciación del atributo implícito en la declaración es opcional. La especificación de multiplicidad 1 también lo es. modelo: Agregación
1 – componente
Componente
Figura 24. Esquema de la relación de agregación 1..1
sintaxis: public class Agregacion { private Componente componente [= new Componente(/* parametros */)]; … }
ejemplo: ColorRGB
1 + color
Cubo
1 # centro
Coord3D
implementación: public class Cubo { protected Coord3D centro = new Coord3D(0.0f, 0.0f, -10.0f); public ColorRGB color = new ColorRGB(127, 127, 127); // gris claro … }
b) Relación 1..n, n 5: Cuando la cantidad de componentes del mismo tipo es relativamente pequeña, digamos n, la clase Agregación deberá incluir n atributos implícitos de tipo Componente , como se puede observar en el modelo de la Figura 25. Nótese que la especificación de la multiplicidad es opcional cuando se escriben los nombres de los atributos en el modelo. En caso de que no se especifiquen el nombre de los atributos, la multiplicidad sí deberá de aparecer. modelo: Agregación
+ componente1 – componente2
Componente
Figura 25. Esquema de la relación de agregación 1.. n, n 5
CAPÍTULO 7: ALMACENAMIENTO DE DATOS 61 implementación: public class Agregacion { public Componente componente1 = new Componente(/* parametros */); private Componente componente2 = new Componente(/* parametros */); … }
ejemplo: Factura
– fecha_Emision – fecha_Pago
Fecha
implementación: public class Factura { private Fecha fecha_Emision; private Fecha fecha_Pago; … }
c) Relación 1..n, n > 5: Cuando la cantidad de componentes del mismo tipo es relativamente pequeña, digamos n, la clase Agregación deberá incluir en su lista de atributos un arreglo de n objetos de tipo Componente , como se puede apreciar en la Figura 26. modelo:
Agregación
n componentes
Componente
Figura 26. Esquema de la relación de agregación 1.. n, n > 5
sintaxis: public class Agregacion { private Componente[] componentes = new Componente[n]; … }
ejemplo: Cubo
8 # vertices
Coord3D
implementación: public class Cubo { protected Coord3D[] vertices = new Coord3D[8]; … }
d) Relación 1..*, 1..+: Cuando la cantidad de componentes del mismo tipo está indefinido, la clase Agregación deberá incluir en su lista de atributos un vector, que almacenará una cantidad variable de objetos de tipo Componente . modelo: Agregación
*ó+ componentes
Componente
Figura 27. Esquema de la relación de agregación 1..*, 1..+
62 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA sintaxis: import java.util.Vector; public class Agregacion { private Vector componentes = new Vector(); … }
ejemplo: + + prod_vendidos
Factura
Detalle
implementación: import java.util.Vector; public class Factura { public Vector prod_Vendidos = new Vector(); … }
7.6 Ejemplo final Se deja al lector como ejercicio la implementación de las clases participantes en el modelo de un sistema de facturación representado en la Figura 28.
Fecha + dia: int + mes: int + anho : int + dia_semana: String
– fecha_emision – fecha_pago
Factura
+ – cliente
1
* no_factura: int – total : float
– prod_vendidos
Cliente * no_cliente: int + nombre: String + direccion: String – saldo : float
+
Detalle
* – producto
1
* no_detalle: int – cantidad : int – subtotal : float
Figura 28. Modelo de un pequeño sistema de facturación
Producto * no_producto: int + nombre: String – precio : float – existencias: int
CAPÍTULO 8
HERENCIA Y POLIMORFISMO
8.1 Introducción La herencia, término tomado de la biología, es una propiedad esencial de la programación orientada a objetos y un mecanismo por el cual se pueden definir nuevas clases en términos de clases ya existentes. La herencia consiste en la transmisión de atributos y comportamientos desde una clase denominada base, hacia otra clase denominada subclase o derivada. Una clase derivada puede ser a su vez una clase base para otras subclases. La herencia nos permite establecer una clasificación jerárquica entre las clases semejante a la existente en la biología con los animales y las plantas. Por ejemplo, a partir de la clase Mamífero se puede crear la subclase Felino, la cual heredará todas las propiedades de un Mamífero, pero además podrá tener otras propiedades particulares. La ventaja principal de la herencia es la reutilización de código. Una vez que una clase ha sido probada y depurada, el código fuente de dicha clase no necesita modificarse para poder ampliar su funcionalidad; esto se puede lograr creando una nueva clase que herede la funcionalidad de la clae base y añada otros comportamientos. En Java, todas las clases derivan de la clase Object de manera implícita, por lo tanto, cualquier clase que definamos tendrá de manera predeterminada los métodos definidos en la clase Object, lo que incluye un constructor sin parámetros. Existen dos paradigmas complementarios que nos llevan a definir clases bases y derivadas a) especialización: cuando se desea ampliar la funcionalidad de un programa sin tener que modificar el código existente; a partir de la clase base creamos la clase derivada que extiende el comportamiento de la clase base b) generalización: cuando nos damos cuenta que divesos objetos guardan un comportamiento en común; a partir de las clases derivadas creamos la clase base que enumerará los atributos y métodos generales
64 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
8.2 Especialización Para entender mejor la especialización, vamos a poner un ejemplo que simule la utilización de librerías de clases para crear entornos gráficos como Windows 9 x. En este ejemplo definiremos una clase base con funcionalidad limitada, para después incrementarla a través de una clase derivada.
8.2.1 Clase base Supongamos que tenemos una clase que describe la conducta de una ventana muy simple, aquella que no dispone de título en la parte superior, por tanto no puede desplazarse, pero si cambiar de tamaño actuando con el ratón en los bordes derecho e inferior. La clase Ventana tendrá los siguientes miembros dato: la posición ( x, y) de la esquina superior izquierda de la ventana y sus dimensiones: ancho y alto. Las funciones miembros, además del constructor serán las siguientes: la función desplegar ( ) que muestra la posición y las dimensiones actuales de la ventana, y la función redimensionar ( ) que nos permite cambiar el tamaño. A continuación se despliega el código de la clase base Ventana . Nótese que todos los atributos de la clase son protegidos, con la intención de que sean accesibles desde las clases derivadas de Ventana . public class Ventana { protected int x; protected int y; protected int ancho; protected int alto; public Ventana(int x, int y, int ancho, int alto) { this.x = x; this.y = y; this.ancho = ancho; this.alto = alto; } public void desplegar(){ System.out.println("Posición: (" + x + ", " + y + ")"); System.out.println("Dimensiones: " + ancho + ", " + alto) } public void redimensionar(int dw, int dh){ ancho += dw; alto += dh; } }
8.2.2 Objetos de la clase base Como vemos en el código, el constructor de la clase base recibe como parámetros los valores iniciales de los atributos. De la siguiente manera creamos un objeto de la clase Ventana: Ventana ventana = new Ventana(5, 10, 25, 30);
A partir del objeto ventana podemos invocar los métodos públicos de la clase Ventana , de la siguiente manera: ventana.desplegar(); ventana.redimensionar(3, -2); ventana.desplegar ();
CAPÍTULO 8: HERENCIA Y POLIMORFISMO
65
El programa imprime el siguiente texto: Posición: (5, 10) Dimensiones: 25, 30 Posición: (5, 10) Dimensiones: 35, 40
8.2.3 La clase derivada Incrementamos la funcionalidad de la clase Ventana definiendo una clase derivada que llamamos Ventana1 . Los objetos de dicha clase tendrán todas las características de los objetos de la clase base, pero además tendrán un título y se podrán desplazar (se simula el desplazamiento de una ventana con el ratón). La clase derivada heredará los atributos y métodos de la clase base; adicionalmente tendrá un atributo para almacenar el título de la ventana y un método para mover la ventana. A continuación se despliega el código de la clase base Ventana1 . Nótese que el atributo titulo es protegido para que pueda ser accesible desde las clases derivadas de Ventana1. public class Ventana1 extends Ventana { protected String titulo; public Ventana1(int x, int y, int ancho, int alto, String titulo) { super(x, y, ancho, alto); this.titulo = titulo; } public void desplegar(){ System.out.println("Título: " + titulo); super.desplegar(); } public void desplazar(int dx, int dy){ x += dx; y += dy; } }
La instrucción extends indica que la clase Ventana1 “extiende a” o “es una subclase de” Ventana . La primera sentencia del constructor de Ventana1 realiza una llamada al constructor de la clase base o superclase mediante la palabra reservada super . A través de esta línea de código se inicializan los atributos x, y, ancho y alto pertenecientes a la clase base. Si en la clase base se define al menos un constructor que tenga parámetros de entrada, las subclases deberán tener por lo menos un constructor en cuya primera línea deberá estar la sentencia super con los parámetros adecuados para invocar a uno los constructores de la superclase. Cuando una subclase implementa un método que tiene el mismo nombre y parámetros de E/S que alguno de la superclase, se efectúa una sobreescritura de métodos . Ventana1 sobreescribe el método desplegar definido en Ventana , para imprimir el título de la ventana y luego invocar el método desplegar de la superclase para imprimir las coordenadas y dimensiones. La sentencia super también se utiliza para tener acceso a los atributos/métodos protegidos y públicos de la super clase, y para distinguirlos de los atributos/métodos del mismo nombre de la subclase. Nótese que si no se escribe la palabra super en la segunda línea del método desplegar de Ventana1 , se estaría invocando de manera recursiva al mismo método.
66 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
8.2.4 Objetos de la clase derivada El constructor de la clase derivada recibe como parámetros los valores iniciales de los atributos. De la siguiente manera creamos un objeto de la clase Ventana1: Ventana1 ventana1 = new Ventana1(5, 7, 60, 34, “Hola”);
A partir del objeto ventana1 podemos invocar los métodos públicos de la clase Ventana1 , y también de la clase base Ventana de la siguiente manera: ventana1.desplazar(10, 5); ventana1.redimensionar(-5, 6); ventana1.desplegar();
El programa imprime el siguiente texto: Título: Hola Posición: (15, 12) Dimensiones: 55, 40
Cabe mencionar que los atributos y métodos protegidos NO son accesibles desde las instancias (objetos) de las clases que las contienen.
8.3 Generalización Para estudiar la generalización vamos a considerar el ejemplo de la jerarquía de clases que describen las figuras planas cerradas, tales como el círculo, el rectángulo y el triángulo. Tales figuras comparten características en común como son la posición y el área; la función para cambiar la posición será el mismo para cualquier tipo de figura, pero el procedimiento para calcular el área será muy particular a cada figura. Imaginemos que se desean crear muchas figuras de distinto tipo y almacenarlas en alguna estructura de datos para después realizar acciones con las figuras, como cálculo de área, despliegue (simulando un editor gráfico) y movimiento, sin importar el tipo de figura del que se trate. Cada vez que obtengamos una figura de nuestra estructura de datos será necesario saber a que clasé pertenece la figura (círculo, rectángulo o triángulo), y tratarla como tal para invocar un método de su clase.
8.3.1 Clase abstracta Para evitar el trabajo de saber a qué clase pertenece una figura, podemos emplear una técnica que nos permitirá tratar todas las figuras por igual e invocar sus métodos sin importar el tipo de figura que se trate. Para ello es necesario definir una clase asbtracta como base de cualquier clase que quiera ser tratada como una figura. Las clases abstractas sólamente se pueden usar como clases base para otras clases. No son instanciables, es decir, no se pueden crear objetos cuya clase sea abstracta. Sin embargo, se pueden declarar variables de dichas clases. Una clase abstracta sólamente puede ser subclase de otra clase abstracta. En tal caso, la subclase abstracta deberá incluir todos los constructores de su superclase.
CAPÍTULO 8: HERENCIA Y POLIMORFISMO
67
Las clases abstractas se utilizan con frecuencia para definir un patrón de comportamiento que tendrán en común algún conjunto de clases: sus subclases. Una clase abstracta puede contener métodos abstractos; éstos métodos no se implementan dentro de la clase abstracta, sino en las clases derivadas. Retomando el ejemplo de las figuras planas, vamos a crear una clase abstracta denominada Figura que guardará las coordenadas actuales de la figura. Tendrá tres métodos públicos: mover , desplegar y obtenerArea , que representan acciones en común entre todas las figuras. La acción desplegar sólamente imprime en pantalla los datos actuales de la figura. Nótese que todas las figuras se mueven de la misma manera: incrementan sus coordenadas de acuerdo a los parámetros de entrada; pero se despliegan y calculan el área de una manera particular. Por lo tanto, los métodos desplegar y obtenerArea serán abstractos y tendrán que implementarse en cada una de las clases que extiendan la clase abstracta Figura. El método imprimirCoordenadas imprime la posición actual de la figura, y podrá ser invocada sólamente en las subclases. A continuación se lista el código de Figura . public abstract class Figura { private int x; private int y; public Figura(int x, int y) { this.x = x; this.y = y; } protected void imprimirCoordenadas() { System.out.println(“Posicion: (” + x + “, ” + y + “)”); } public void mover(int dx, int dy) { x += dx; y += dy; } public abstract void desplegar(); public abstract double obtenerArea(); }
8.3.2 Clases derivadas A continuación se lista el código de alguna de las clases derivadas de Figura; como estas clases son subclases de una clase abstracta deberán incluir la sentencia extends en la definición de la clase seguida del nombre de la superclase; además deberán implementar los métodos abstractos desplegar y obtenerArea definidos en Figura. En las subclases estos métodos no son abstractos. Finalmente, cada subclase podrá tener diferentes atributos de acuerdo al tipo de figura del que se trate. public class Circulo extends Figura { protected double radio; public Circulo(int x, int y, double radio){ super(x, y); this.radio = radio; } public void desplegar() { System.out.println(“Circulo”); imprimirCoordenadas(); System.out.println(“Radio = ” + radio); } public double obtenerArea() { return Math.PI * radio * radio; } }
68 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA public class Rectangulo extends Figura { protected double base, altura; public Rectangulo(int x, int y, double base, double altura){ super(x,y); this.base = base; this.altura = altura; } public void desplegar() { System.out.println(“Rectangulo”); imprimirCoordenadas(); System.out.println(“Base = ” + base + “, altura = ” + altura); } public double obtenerArea() { return base * altura; } } public class Triangulo extends Figura { protected double base, altura; public Triangulo(int x, int y, double base, double altura){ super(x,y); this.base = base; this.altura = altura; } public void desplegar() { System.out.println(“Triangulo”); imprimirCoordenadas(); System.out.println(“Base = ” + base + “, altura = ” + altura); } public double obtenerArea() { return base * altura / 2; } }
8.3.3 Objetos de una clase derivada Vamos a crear un objeto de tipo Circulo e invocar métodos propios de su clase y de la clase base Figura . El constructor recibe como parámetros los valores iniciales de los atributos. De la siguiente manera creamos un objeto de la clase Circulo: Circulo circulo = new Circulo(5, 10, 75.3); circulo.mover(7, -3); circulo.desplegar(); System.out.println(“Area = ” + circulo.obtenerArea());
El programa imprime el siguiente texto: Circulo Posición: (15, 12) Radio = 75.3 Area = 17813.113
8.3.4 Enlace dinámico En el lenguaje C, como en la mayoría de los lenguajes estructurados, los identificadores de la función están asociados siempre a direcciones físicas antes de la ejecución del programa, esto se conoce como enlace estático. Ahora bien, los lenguajes C++ y Java permiten decidir a que función llamar en tiempo de ejecución, esto se conoce como enlace dinámico. A continuación, se presenta un ejemplo de ello utilizando las clases derivadas de Figura.
CAPÍTULO 8: HERENCIA Y POLIMORFISMO
69
Si creamos un arreglo de tipo Figura , cada casilla podrá hospedar ya sea objetos de tipo Figura u objetos cuya clase sea derivada de Figura. En nuestro caso, como Figura es una clase abstracta, no la podemos instanciar, por lo tanto, vamos a guardar únicamente objetos de tipo Triángulo , Rectángulo o Círculo. Esto es permisible porque estas 3 clases son derivados de Figura. Figura[] figuras figuras[0] = new figuras[1] = new figuras[2] = new figuras[3] = new
= new Figura[4]; Circulo(15, 37, 4.7); Rectangulo(12, 3, 6.5, 3.5); Triangulo(60, 58, 6, 7.8); Circulo(0, 0, 3.0);
La sentencia: figuras[i]. mover(13, 10)
invocará al método mover propio de la clase base Figura; la decisión de qué función invocar se toma en momento de compilar el programa. En este caso, se dice que se lleva a cabo un enlace estático. Pero, la sentencia: figuras[i].obtenerArea()
¿a qué método obtenerArea va a invocar, al de Circulo, Rectangulo o Triangulo? La respuesta es: según sea el valor del índice i. Si i = 0, se invocará el método obtenerArea de la clase Circulo porque el primer elemento del arreglo guarda una referencia a un objeto de esta clase. Si el índice es 1, entonces se invocará el método obtenerArea propio de la clase Rectangulo, y así sucesivamente. La decisión de qué función invocar se toma en el momento de la ejecución del programa. En estos casos, se dice que se lleva a cabo un enlace dinámico. Es importante saber que si el método obtenerArea no estuviera definido en la clase abstracta Figura , no podriá ser invocado desde ningún elemento del arreglo, a menos que se realizara un cast con el nombre de la subclase.
8.3.5 Polimorfismo en acción Supongamos que deseamos saber cuál es la figura que tiene mayor área, independientemente de su forma. Para ello, codificamos un método que reciba un arreglo de figuras, como la definida arriba, y que devuelva una referencia a la figura con área mayor. Este método, obtenerFiguraMayor , tendrá la ventaja principal de que está definida en términos de variables de tipo Figura, por tanto, trabaja no sólamente para una colección de círculos, rectángulos y triángulos, sino también para cualquier otra figura derivada de la clase base Figura. De esta manera, si se deriva Pentágono de Figura y se añade a la jerarquía de clases, este método podrá manejar objetos de dicha clase, sin modificar para nada el código del mismo. La codificación del método es como sigue: public Figura obtenerFiguraMayor(Figura[] figuras) { Figura figuraMayor = null; double areaMayor = figuras[0].obtenerArea(); for(int i = 1; i < figuras.length; i ++) { if(figuras[i].obtenerArea() > areaMayor) { figuraMayor = figuras[i]; areaMayor = area; } } return figuraMayor; }
70 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
Esta es la llamada al método obtenerFiguraMayor , utilizando el arreglo definido anteriormente: Figura figuraMayor = obtenerFiguraMayor(figuras); System.out.println(“El area mayor es: ” + figuraMayor. obtenerArea());
Se deberá imprimir el siguiente mensaje El area mayor es: 69.397781
que corresponde al primer círculo
Para conocer el valor del área, se invoca el método obtenerArea a partir de figuraMayor . El método que realmente se va a invocar dependerá del tipo de objeto al que hace referencia la variable figuraMayor . Si guarda una referencia a un objeto Circulo, llamará al método obtenerArea definido en dicha clase. De manera similar para las otras figuras. La combinación de herencia y enlace dinámico se denomina polimorfismo. El polimorfismo es, por tanto, la técnica que permite pasar un objeto de una clase derivada a funciones que conocen el objeto sólamente por su clase base. El polimorfismo nos ayuda a hacer los programas más flexibles, porque en el futuro podemos crear nuevas clases derivadas de la clase abstracta Figura sin tener que modificar los métodos que los utilizan.
8.4 Herencia múltiple La herencia múltiple consiste en definir una clase en términos de varias clases ya existentes, esto es, la subclase heredará el comportamiento de todas sus superclases. En el lenguaje C++ es posible implementar la herencia múltiple permitiendo que una clase extienda a muchas clases, pero este tipo de herencia puede presentar dificultades. Por ejemplo, si dos clases B y C derivan de una clase base A, y a su vez una clase D deriva de B y C, se presenta un problema conocido con el nombre de diamante. Supongamos que A tiene un atributo protegido x, entonces B y C heredarán ese atributo con el mismo nombre. Pero como D hereda todos los atributos protegidos de B y C, tendrá 2 atributos implícitos con el mismo nombre x. Esta situación de conflicto de nombres se despliega en la Figura 29. Para evitar este tipo de problemas, el lenguaje Java sólamente permite heredar de una clase a la vez, pero de todos modos sí nos permite implementar la herencia múltiple a través del uso de interfaces.
# x : ti o
B
x de A
x de B
x de A
D
C
x de C
conflicto
Figura 29. Problema del diamante en herencia múltiple
CAPÍTULO 8: HERENCIA Y POLIMORFISMO
71
8.4.1 Interfaces Una interfaz es un protocolo de comportamiento que nos permite describir algunas de las características que tendrá alguna clase, pero sin definir su personalidad completa. Consiste en una colección de constantes y declaraciones de métodos no implementados. De manera implícita , estos métodos son abstractos, públicos y estáticos. Una interfaz, como una clase abstracta, no puede ser instanciada, pero sí podemos declarar una variable de tipo interfaz. Una clase abstracta es extendida por las subclases a través de una estructura jerárquica; una interfaz, en cambio, es implementada por una o más clases que no guardan necesariamente una relación jerárquica. Tales clases debéran implementar todos los métodos y podrán hacer uso de las constantes, ambos definidos en la interfaz. Las interfaces definen similitudes entre clases sin forzar su relación, debido a que permiten que clases no relacionadas puedan tener comportamientos similares: dos clases que implementan la misma interfaz pueden no estar relacionadas en la jerarquía de clases. Una clase sólamente puede derivarse de una clase base, pero puede implementar varias interfaces. Una interfaz a su vez puede extendar una o más interfaces, esto significa que entre interfaces sí hay herencia múltiple. A continuación se presenta la sintaxis para la definición de interfaces. El nombre del archivo deberá ser el mismo que el nombre de la interfaz, y la extensión será .java. El nivel de visibilidad de las constantes y de los métodos es público por defecto; además, las constantes son estáticas. sintaxis: public interface Interfaz { tipo_dato1 constante1 = valor1; … tipo_datom constante m = valorm; tipo_devuelto1 metodo1(/* parametros de entrada */); … tipo_devuelton metodon(/* parametros de entrada */); }
8.4.2 Clase implementadora Así como las clases abstractas, las interfaces no sirven de mucho si no existe al menos una clase que las implemente: la clase implementadora . Esta clase deberá realizar todos los métodos definidos en cada una de las interfaces que la clase implemente; tales métodos deberán ser públicos y no estáticos. A continuación se presenta la sintaxis para la clase implementadora, resaltando en negrita las sentencias importantes. sintaxis: public class Clase1 implements Interfaz1, …, Interfazn { public … public … public … public }
tipo_devuelto1 metodo1DeInterfaz1(/* parametros de entrada */); tipo_devueltom metodomDeInterfaz1(/* parametros de entrada */); tipo_devuelto1 metodo1DeInterfazn(/* parametros de entrada */); tipo_devuelton metodomDeInterfazn(/* parametros de entrada */);
72 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
8.4.3 Herencia múltiple en acción En esta sección llevaremos a la práctica la herencia múltiple utilizando una clase abstracta denominada Animal, una interfaz Volador , y 2 clases concretas: Hombre, Superman . La clase Hombre hereda el comportamiento de Animal; la clase Superman extiende a la clase Hombre e implementa además la interfaz Volador . De esta manera, Superman exhibe propiedades de un Hombre y de un ser Volador . A continuación se presenta la clase Animal que define el comportamiento básico de un animal. public abstract class Animal { protected final int DORMIDO = 0; protected final int DESPIERTO = 1; protected final int MUERTO = 2; protected int estado = DORMIDO; public void dormir() { if(estado == DESPIERTO) { estado = DORMIDO; System.out.println(“... durmiendo ...”); } } public void despertar() { if(estado == DORMIDO) { estado = DESPIERTO; System.out.println(“... despertando ...”); } } public void morir() { if(estado != MUERTO) { estado = MUERTO; System.out.println(“... muriendo ...”); } } public abstract void caminar(); public abstract void comer(); }
La clase Hombre hereda el comportamiento de Animal, implementa los métodos caminar y comer , además de agregar la acción pensar . public class Hombre extends Animal { public void caminar() { System.out.println(“caminando con mis 2 pies”); } public void comer() { System.out.println(“comiendo con cubiertos”); } public void pensar() { System.out.println(“... pensando ...”); } }
La interfaz Volador define el comportamiento básico de un objeto que tenga las facultades de volar. public interface Volador { String tipo = “Volador”; void ascender(int metros);
CAPÍTULO 8: HERENCIA Y POLIMORFISMO
73
void descender(int metros); void aterrizar(); }
La clase Superman extiende a la clase Hombre e implementa la interfaz Volador , por lo que tendrá que implementar todos los métodos de Volador ; los métodos de Animal ya no tiene que implementarse aquí porque ya se realizaron en Hombre. De esta manera, Superman presenta herencia múltiple. public class Superman extends Hombre implements Volador { private int poder = 100; public void debilitarse(int kriptonita) { poder -= kriptonita; System.out.println(“oh oh! Hay kriptonita!”); } public void fortalecerse(int fuerza) { poder += fuerza; System.out.println(“Fortaleciendome!!”); } public void ascender(int metros) { System.out.println(“Ascendiendo ” + metros + “ metros”); } public void descender(int metros) { System.out.println(“Descendiendo ” + metros + “ metros”); } public void aterrizar() { System.out.println(“Aterrizando”); } }
A continuación se presenta un código donde se crea un objeto de tipo Superman , y se realizan todas las acciones que pueda llevar a cabo Superman . La primera línea despliega la constante que hereda de la interfaz Volador ; recordar que las constantes de una interfaz son estáticas de manera predeterminada. System.out.println(Superman.tipo); Superman s = new Superman(); s.despertar(); s.pensar(); s.comer(); s.fortalecerse(50); s.caminar(); s.ascender(10); s.debilitarse(10); s.descender(5); s.aterrizar(); s.dormir(); s.morir();
El siguiente texto se verá impreso en pantalla tras la ejecución del código anterior: Volador ... despertando ... ... pensando ... comiendo con cubiertos Fortaleciendome!! caminando con mis 2 pies Ascendiendo 10 metros oh oh! Hay kriptonita!
74 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA Descendiendo 5 metros Aterrizando ... durmiendo ... ... muriendo ...
8.4.4 Polimorfismo en acción A través del siguiente ejemplo, veremos que la importancia de las interfaces no estriba en resolver los problemas inherentes a la herencia múltiple sin forzar relaciones jerárquicas, sino en incrementar el polimorfismo del lenguaje más allá del que proporciona la herencia simple. El método despertarAnimal puede recibir como parámetro cualquier objeto cuya clase sea descendiente de la clase abstracta Animal. Este tipo de polimorfismo es propio de la herencia simple. public void despertarAnimal( Animal a) { a.despertar(); }
El método aterrizarVolador puede recibir como parámetro cualquier objeto cuya clase implemente la interfaz Volador , sin importar la estructura jerárquica a la que pertenezca dicha clase. Este tipo de polimorfismo –más sofisticado– es propio de la herencia múltiple a través del uso de interfaces en Java. public void aterrizarVolador( Volador v) { v.aterrizar(); }
En las siguientes líneas de código se crea un objeto de tipo Superman y se invocan los métodos anteriores utilizando como parámetro el objeto creado. Superman s = new Superman(); despertarAnimal(s); // imprime ... durmiendo ... aterrizarVolador(s); // imprime Aterrizando
Si creamos una clase Avion que herede de alguna clase abstracta llamada Vehiculo (independiente de Animal) y que además implemente la interfaz Volador , podrá ser pasada como parámetro para el método aterrizarVolador . Extendemos la definición de polimorfismo enunciada en el Capítulo 8.3.5 de la siguiente manera: “técnica que permite pasar un objeto como parámetro a funciones que conocen al objeto sólamente por su clase base o por la interfaz que implementa su clase”.
8.4.5 Modelado de interfaces usando notación UML Una interfaz se representa con UML en cursiva –como la clase abstracta– y entre paréntesis angulares. Una clase deberá apuntar en el diagrama hacia la interfaz que implemente, utilizando una flecha similar a la de la herencia, pero el triángulo es sombreado. Esto se puede apreciar en la Figura 30. La Figura 31 despliega el diagrama UML para el ejemplo visto en la Sección 8.4.3.
CAPÍTULO 8: HERENCIA Y POLIMORFISMO
Clase implementadora
Figura 30. Representación de la implementación de interfaces con UML
Animal
Hombre
Superman
Figura 31. Modelo de la aplicación presentada en el Capítulo 8.4.3
75
CAPÍTULO 9
EXCEPCIONES
Los programadores de cualquier lenguaje se esfuerzan por escribir programas libres de errores, sin embargo, es muy difícil que los programas reales se vean libres de ellos. En Java las situaciones que pueden provocar un fallo en el programa se denominan excepciones. Java lanza una excepción en respuesta a una situación poco común. El programador también puede lanzar sus propias excepciones. Las excepciones en Java son objetos de clases derivadas de la clase base Exception. Existen también los errores internos que son objetos de la clase Error que no se estudiarán en este capítulo. Ambas clases son derivadas de la clase base Throwable. Existe toda una jerarquía de clases derivada de la clase base Exception. Estas clases derivadas se ubican en dos grupos principales: 1) Las excepciones en tiempo de ejecución ocurren cuando el programador no ha tenido cuidado al escribir su código. Por ejemplo, cuando se sobrepasa la dimensión de un arreglo se lanza la excepción ArrayIndexOutOfBounds . Cuando se hace uso de una referencia a un objeto que no ha sido creado se lanza la excepción NullPointerException. Estas excepciones le indican al programador qué tipos de fallos tiene el programa y que debe arreglarlo antes de proseguir. 2) Las excepciones que indican que ha sucedido algo inesperado o fuera de control En este capítulo veremos las excepciones en tiempo de ejecución
9.1 Excepciones comunes En la Sección 3.6 se vieron funciones para convertir cadenas de texto a valores numéricos. Estas funciones son muy usadas cuando creamos aplicaciones visuales. El usuario introduce un número en algún control de edición de texto, se obtiene el contenido del texto y se convierte a su valor numérico para guardarse en alguna variable. Si el usuario no escribió un número válido, al momento de intentar convertir el texto a número, Java generará una excepción en tiempo de ejecución, desplegando un mensaje en la consola. A continuación se despliega un ejemplo que genera este tipo de excepción: int edad = new Integer("35A").intValue(); ó int edad = Integer.parseInt(“35 ”);
78 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
Si se introducen caracteres no numéricos o no se quitan los espacios en blanco al principio y al final del string, mediante la método trim( ) de String, se lanza una excepción NumberFormatException . En muchas ocasiones, por mala programación, intentamos utilizar un atributo o invocar un método de un objeto que no ha sido previamente inicializado. String cadena; int l = cadena.length();
El compilador nos suele avisar con el mensaje “variable cadena might not have been initialized”. En otras ocasiones, el compilador no puede tener la certeza de que un objeto no ha sido creado o se ha liberado previamente, por lo tanto, no suceden errores al compilar. Pero, al ejecutar la aplicación, se lanza la excepción NullPointerException cuando se intenta usar un objeto que hace referencia a ningún espacio de memoria. public void dibujarTriangulo(Triangulo t) { t.desplegar(); // si t no esta inicializado, se lanza una excepcion }
Otra excepción que vemos con mucha frecuencia es ArrayIndexOutOfBoundsException , la cual se desencadena cuando intenamos realizar un acceso a un elemento de un arreglo que no existe. En el siguiente método se va a generar esta excepción durante la última iteración del ciclo for, después de haber impreso los primeros numeros.length-1 numeros del arreglo. La excepción ocurre porque no existe el elemento número numeros.length , ya que la numeración en Java comienza desde cero. public void desplegar(int[] numeros) { for(int i = 0; i <= numeros.length; i ++) System.out.println(numeros[i]); }
Existen excepciones que suceden muy a menudo cuando se ejecutan funciones de Entrada / Salida; por lo tanto, Java obliga al programador a capturar una excepción cada vez que se ejecuta alguna de estas funciones. Una función típica de E/S es la siguiente: System.in.read();
Cuando compilamos el programa, nos aparece un mensaje de error indicando que se debe lanzar o capturar la excepción IOException.
9.2 Captura de excepciones Toda sucesión de instrucciones que sea susceptible de desencadenar al menos una excepción durante su ejecución deberá estar contenida en un bucle de excepción, de la siguiente manera: try { // sentencias que pueden generar excepciones en tiempo de ejecucion } catch(Exception e) { // acciones a realizar si sucede una excepcion }
CAPÍTULO 9: EXCEPCIONES 79
El bloque try nos permite proteger la ejecución de un programa de terminaciones inesperadas debido a errores inesperados, de programación o de entradas de usuario. Como su nombre lo indica, try le dice a la máquina virtual de Java que “intente” ejecutar el siguiente bloque de código; si una de las sentencias colocadas dentro del bloque es susceptible de desencadenar una excepción, Java crea un objeto e de tipo Exception y se ejecutan las sentencias contenidas en el bloque catch . En caso contrario, las sentencias del bloque try se pueden realizar sin problemas El siguente método recibe 2 números enteros y devuelve el resultado de dividir el primero entre el segundo. Si el segundo número es 0, la división generará una excepción porque el resultado es infinito. Por lo anterior, es necesario colocar la operación dentro de un bloque try para indicarle a Java que primero “intente” realizar la operación; si fuera inminente la aparición de una excepción como resultado la división, se despiega un mensaje de error informando al usuario y el resultado será 0, en caso contrario, se realiza la operación con seguridad. ejemplo: public int cociente(int a, int b) { int c = 0; try { c = a / b; } catch(Exception e) { System.out.println(“Division entre cero”); } return c; }
Nótese que en el código anterior estamos tratando la excepción de manera general, porque estamos tratando al objeto creado por Java como un objeto de la clase Exception, la cual es la clase base para definir cualquier excepción. Es importante mencionar que si el resultado se guarda en una variable real ( float , double) no se genera la excepción, sino que el resultado será infinity.
9.2.1 Capturando varias excepciones En ocasiones nos encontramos con la situación de que dentro de un bloque try se pueden desencadenar dos o más excepciones de diferente tipo. Si queremos saber exactamente qué error sucedió para tratar cada uno de los casos de diferente manera, agregamos un bloque catch por cada excepción diferente que pueda suceder y especificamos el nombre de la excepción en lugar de usar la clase Exception. El siguiente segmento de código ilustra la manera en que podemos capturar varias excepciones. El programa divide dos números introducidos por el usuario a través de controles de edición, y despliega el resultado o algún mensaje de error (en caso de suceder). Se utilizan dos variables de tipo String para guardar estos números. Para poder realizar la operación, las variables tienen que convertirse a números. En esta aplicación se pueden producir dos excepciones diferentes: NumberFormatException , si se introducen caracteres no numéricos, y ArithmeticException si se divide entre cero. public static void cociente(String num1, String num2) { String mensaje; int numerador, denominador, cociente; try { numerador = Integer.parseInt(num1); denominador = Integer.parseInt(num2); cociente = numerador / denominador; mensaje = new Integer(cociente).toString() ;
80 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA } catch(NumberFormatException ex){ mensaje = "Se han introducido caracteres no numericos"; } catch(ArithmeticException ex) { mensaje = "Division entre cero"; } System.out.println(mensaje); }
En ciertas ocasiones se desea estar seguro de que un bloque de código se ejecute se generen o no excepciones. Podemos asegurar su ejecución añadiendo un bloque finally después del último catch. Esto es importante cuando accedemos a archivos, para asegurar que se cerrará siempre un archivo se produzca o no un error en el proceso de lectura / escritura, o para llevar algun historial de todas las excepciones que pudieron ocurrir y si el programa se ha recuperado de ellas o no. sintaxis: try { // Este codigo puede generar una excepcion } catch(Exception e) { // Este codigo se ejecuta cuando se produce una excepción } finally { // Este codigo se ejecuta se produzca o no una excepción }
9.3 Creando excepciones propias Java proporciona las clases que manejan casi cualquier tipo de excepción. Sin embargo, podemos imaginar situaciones en las que producen excepciones que no están dentro del lenguaje Java. Extendiendo el ejemplo de la sección anterior, estudiaremos una situación en la que se reciben parámetros fuera de un rango determinado, para que el programa lanza un excepción que llamaremos IntervalException. Esta clase extender la clase base Exception. public class IntervalException extends Exception { public IntervalException(String mensaje) { super(mensaje); // acciones que se deseen tomar una vez creada la excepcion } }
La definición de la clase es muy simple: el constructor recibe como parámetro una cadena de texto con la descripción del error que sucedió, y a su vez lo manda al constructor de la clase base Exception mediante la sentencia super . En las siguiente líneas de código se escribirán todas aquellas acciones que se deseen llevar a cabo una vez creado el objeto Exception, por ejemplo, desplegar algún cuadro de diálogo que contenga información textual o gráfica acerca del error.
9.3.1 Métodos que lanzan excepciones En Java podemos definir métodos que lancen durante su ejecución una o más excepciones propias o del lenguaje. La invocación de tal método deberá estar localizada en un bloque try. Un método que lanza una excepción tiene la declaración habitual de cualquier otro método pero se le añade al final la palabra reservada throws seguida de la excepción o excepciones que puede lanzar separadas con comas, como se muestra en el siguiente ejemplo:
CAPÍTULO 9: EXCEPCIONES 81 ejemplo: public void rango(int a) throws IntervalException { if((a > 100) || (a < 0)) throw new IntervalException("Valor fuera de rango"); }
Cuando el numero a recibido sea mayor que 100 o negativo se lanza una excepción mediante la instrucción throw, donde se crea un objeto de la clase IntervalException. Dicho objeto se crea llamando al constructor de dicha clase y pasándole un string que contiene el mensaje "Valor fuera de rango". El siguiente ejemplo ilustra la forma en que podemos definir métodos que lancen más de una excepción propia o de Java. Nótese que las operaciones de división y de conversión de tipos ya no se encuentran dentro de un bloque try. ejemplo: static int calcular(String a, String b) throws IntervalException, NumberFormatException, Ar ithmeticException { int num = Integer.parseInt(a); int den = Integer.parseInt(b); if((num > 100) || (den < -100)) throw new IntervalException("Números fuera del intervalo"); return (num / den); }
La invocación de los métodos rango y calcular deberán estar situadas dentro de un bloque try.
9.3.2 Captura de las excepciones propias En el siguiente código se extiende la funcionalidad del método cociente presentado en la Sección 9.2.1 para presentar la manera en que capturamos excepciones propias e invocamos métodos que lancen excepciones. El método rango verifica si el numerador sobrepasa el número 100 y si el denominador es menor a -100. El método getMessage ( ) de la clase Exception nos devuelve la descripción textual de la excepción que ha ocurrido. En el código siguiente, devuelve la cadena “Valores fuera de rango” public class Excepciones { static void divisionEntera(String num1, String num2) { String mensaje; int numerador, denominador, cociente; try { numerador = Integer.parseInt(num1); denominador = Integer.parseInt(num2); rango(numerador, denominador); cociente = numerador / denominador; mensaje = new Integer(cociente).toString() ; } catch( NumberFormatException ex){ mensaje = "Se han introducido cantidades no validas"; } catch( ArithmeticException ex){ mensaje = "Division entre cero"; } catch(IntervalException ex){ mensaje = ex.getMessage(); } System.out.println(mensaje); } static void rango(int num, int den) throws IntervalException { if((num > 100) || (den < -100)) throw new IntervalException("Valores fuera de rango");
82 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA } public static void main(String[] args) { divisionEntera(“90”, “-10”); } }
9.4 Ciclo de vida de una excepción El ciclo de vida de una excepción se puede resumir del siguiente modo: 1. 2. 3. 4. 5.
Se coloca la llamada a la función susceptible de producir una excepción en un bloque try..catch En dicha función, se crea un objeto de la clase Exception o de una clase derivada Se lanza mediante throw el objeto recién creado Se captura en el correspondiente bloque catch En el bloque catch se notifica al usuario acerca del error sucedido, imprimiendo el mensaje asociado a dicha excepción, o realizando una tarea específica.
CAPÍTULO 10
HILOS
Un hilo o thread en informática es un proceso individual ejecutándose en un sistema y representa un flujo de control del programa. A veces se les llama procesos ligeros o contextos de ejecución. Normalmente, cada hilo se crea para controlar un único aspecto dentro de un programa, como puede ser supervisar la entrada en un determinado periférico, gestionar alguna animación, realizar búsquedas de información o controlar toda la entrada/salida del disco. Todos los hilos comparten los mismos recursos, a diferencia de los procesos en donde cada uno tiene su propia copia de código y datos (separados unos de otros).
10.1 Flujos de un programa Un programa de flujo único o mono-hilvanado (single-threaded ) utiliza un único flujo de control para controlar su ejecución. Muchos programas no necesitan la potencia que proporciona el uso de múltiples flujos de control. Sin necesidad de especificar explícitamente que se quiere un único flujo de control, muchos de los applets y aplicaciones son de flujo único (de manera predeterminada). Por ejemplo, en la siguiente aplicación trivial: public class UnHilo { public static void main(String[] args) { System.out.println( "Este programa tiene un solo hilo" ); } }
cuando se llama al método main(), la aplicación imprime el mensaje y termina. Esto ocurre dentro de un único hilo: el predeterminado. En esta aplicación, no vemos el hilo que ejecuta nuestro programa, sin embargo, Java posibilita la creación y control de hilos de manera explíicita. La simplicidad para crear, configurar y ejecutar hilos permite que se puedan implementar aplicaciones muy poderosas y de buen desempeño que no se puede con otros lenguajes de tercera generación. En un lenguaje orientado a Internet como es Java, esta herramienta es vital. Si se ha utilizado un navegador con soporte Java, se habrá visto el uso de múltiples threads en Java. Podemos observar que dos applets se pueden ejecutar al mismo tiempo o que podemos desplazar la página del navegador mientras el applet continúa ejecutándose. Esto no significa que el applet utilice múltiples hilos , sino que el navegador es multi-hilo ( multi-threaded ).
84 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
Las aplicaciones y applets multi-hilo utilizan muchos contextos de ejecución para cumplir su trabajo. Hacen uso del hecho de que muchas tareas contienen subtareas distintas e independientes. Se puede utilizar un hilo para cada subtarea. Mientras que los programas de flujo único pueden realizar su tarea ejecutando las subtareas secuencialmente. Un programa mulit-hilo permite que cada hilo comience y termine tan pronto como sea posible. Este comportamiento presenta una mejor respuesta a la entrada en tiempo real.
10.2 Creación de un hilo Jara proporciona 2 técnicas para crear hilos en Java. Una es extendiendo la clase Thread, y la otra consiste en implementar la interfaz runnable. La segunda técnica es la más habitual y la que utilizaremos en los ejemplos de este capítulo. La siguiente clase representa el primer modo de crear un hilo. La presencia de las sentencias en negrita es fundamental para esta técnica. public class MiHilo extends Thread { public void run() { … } }
El ejemplo anterior crea una clase MiHilo que hereda todas las propiedades de un hilo, ya que extiende a la clase Thread; además, esta clase sobrecarga el método Thread.run() por su propia implementación. El contenido del método run() representa todo el trabajo que va a realizar el hilo una vez que esté en ejecución. Extendiendo la clase Thread, se pueden heredar los métodos y variables de la clase padre. Una de las limitantes de esta técnica radica en el hecho de que Java solamente puede extender una clase a la vez; esto significa que una clase que extienda a Thread para poder ser tratada como un hilo, ya no puede heredar de otra clase. Esta situación puede ser solucionada utilizando la segunda técnica: implementar la interfaz Runnable, como se aprecia en el siguiente ejemplo: public class MiHilo implements Runnable { int inicio = 0; public MiHilo(int inicio) { this.inicio = inicio; } public void run() { for(int i = inicio; i < inicio + 100; i ++) System.out.print(i + “ ”); } }
Para poner a funcionar este hilo, creamos una objeto de la clase Thread, mandando como parámetro una instancia de la clase MiHilo; enseguida invocamos el método start ( ) propio de la clase Thread. Esto se puede apreciar en el siguiente código: Thread thread1 = new Thread (new MiHilo(100)); thread1.start(); Thread thread2 = new Thread (new MiHilo(350)); thread2.start();
CAPÍTULO 10: HILOS 85
En el proceso anterior se recorren 2 ciclos de manera simultánea, uno que cuenta del 100 al 200 y otro que cuenta del 350 al 450. Los hilos se ejecutan aparentemente de forma paralela.
10.3 Estados de un hilo Durante el ciclo de vida de un hilo, éste se puede encontrar en diferentes estados. El diagrama presentadoen la Figura 32 se aproxima al funcionamiento real de un hilo, mostrando los estados por los que puede pasar y los eventos que provocan el paso de un estado a otro. yield ( ) new Thread ( )
Nuevo
start ( )
Ejecutable
stop ( )
stop ( ) ó run ( ) terminado
Suspendido
stop ( )
Muerto
Figura 32. Estados de un hilo
10.3.1 Nuevo La siguiente sentencia crea un nuevo hilo pero no lo arranca, lo deja en el estado de Nuevo: Thread miThread = new Thread(miRunnable);
Cuando un hilo se encuentra en este estado es simplemente un objeto Thread vacío. El sistema no ha destinado ningún recurso para él. Desde este estado solamente puede arrancarse llamando al método start ( ), o detenerse definitivamente, llamando al método stop( ); la llamada a cualquier otro método no tendrá razón de ser y provocará la generación de la excepción IllegalThreadStateException .
10.3.2 En ejecución La llamada al método start ( ) creará los recursos del sistema necesarios para que el hilo pueda ejecutarse, lo incorpora a la lista de procesos disponibles para ejecución del sistema y llama al método run( ) de la clase Thread. miThread.start();
En este momento nos encontramos en el estado Ejecutable del diagrama. Se le llama Ejecutable y no en Ejecución, porque cuando el hilo está aquí no esta corriendo. Muchos computadoras tienen sólamente un procesador lo que hace imposible que todos los hilos estén corriendo al mismo tiempo. Java implementa un tipo de calendarización ( scheduling ) o lista de procesos, que permite que el procesador sea compartido entre todos los hilos que se encuentran en la lista. Sin embargo, en la mayoría de los casos, se puede considerar que este estado es realmente un estado En Ejecución, porque la impresión
86 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
que produce ante nosotros es que todos los procesos se ejecutan al mismo tiempo. Cuando el hilo se encuentra en este estado, todas las instrucciones de código que se encuentren dentro del bloque declarado para el método run( ), se ejecutarán secuencialmente. La accción yield ( ) del diagrama de estados indica que se le ha asignado el procesador a un hilo Ejecutable para realizar la siguiente acción.
10.3.3 Suspendido Un hilo entra en estado Suspendido cuando sucede uno de los 4 casos siguientes: 1) 2) 3) 4)
se invoca al método suspend ( ), se llama al método sleep(milisegundos), el hilo está bloqueado en un proceso de entrada/salida el hilo utiliza su método wait ( ) para esperar a que se cumpla una determinada condición
A continuación se extiende la clase MiHilo para que presente un retardo específico después de imprimir cada número. public class MiHilo implements Runnable { int inicio, retardo; public MiHilo(int inicio, int retardo) { this.inicio = inicio; this.retardo = retardo; } public void run() { for(int i = inicio; i < inicio + 10; i ++) { System.out.println(i); try { Thread.sleep(retardo); } catch(InterruptedException e) } } } public static void main(String[] args) { Thread t1 = new Thread(new MiHilo(200, 100)); t1.start(); Thread t2 = new Thread(new MiHilo(500, 50)); t2.start(); } }
La ejecución de la clase MiHilo despliega lo siguiente en la consola: 200 500 501 201 502 503 202 504 505 203 506 507 204 508 205 509 206 207 208 209
Como el segundo hilo tiene la mitad del retardo que el primer hilo, por cada número que despliega el primer hilo se despliegan 2 del segundo hilo, hasta que termina la ejecución del segundo hilo. El método sleep propio de la clase Thread es estático y por lo tanto puede llamarse sin crear instancias de Thread. El parámetro de entrada de sleep es de tipo entero y representa los milisegundos que se tendrán que esperar para realizar la siguiente acción del hilo que se durmió. La invocación a este método debe estar situado en un bloque try ya que puede generar la excepción InterruptedException en caso que algún proceso externo detenga el hilo mientras éste esté durmiendo. Cuando se ejecuta la sentencia Thread.sleep , el hilo que la contiene pasa al estado Suspendido y tendrá que esperar los
CAPÍTULO 10: HILOS 87
milisegundos especificados en el método para volver al estado Ejecutable, incluso aunque el procesador estuviera totalmente libre. Para cada una de los cuatro modos de entrada al estado Suspendido hay una forma específica de volver al estado Ejecutable. Cada forma de recuperar ese estado es exclusiva; por ejemplo, si el hilo se ha puesto a dormir, una vez transcurridos los milisegundos que se especifiquen, él solo se despierta y vuelve al estado Ejecutable. Llamar al método resume( ) mientras esté el hilo durmiendo no serviría para nada. Los métodos de recuperación del estado Ejecutable, en función de la forma de llegar al estado Suspendido del hilo, son los siguientes: 1. Si un hilo está dormido, una vez que pasó el lapso de tiempo 2. Si a un hilo se le ejecutó el método suspend ( ), luego de una llamada al método resume() 3. Si un hilo está bloqueado por alguna acción de entrada / salida, una vez que el comando E/S concluya su ejecución 4. Si un hilo está esperando una condición con el método wait ( ), cada vez que la variable que controla esa condición varíe debe llamarse a notify() o notifyAll()
10.3.4 Muerto Cuando un hilo pasa al estado Muerto se liberan todos los recursos (procesador, memoria, E/S) que necesitó para su ejecución. No podremos volver a ejecutar el método start sobre un hilo muerto. Un hilo puede morir de 2 formas: a) cuando concluye de manera satisfactoria la ejecución del método run( ) b) cuando se invoca el método stop( ) En la clase MiHilo, definida en la sección anterior, se crean 2 hilos llamados t1 y t2, los cuales pasarán al estado Muerto automáticamente cuando se finalice el ciclo for colocado en el método run. Los applets utilizan el método stop() para matar a todos sus hilos cuando el navegador con soporte Java en el que se están ejecutando le indica al applet que se detengan, por ejemplo, cuando se minimiza la ventana del navegador o cuando se cambia de página. La clase Thread incluye el método isAlive(), que devuelve true si el hilo ha sido arrancado y no ha sido detenido. Por ello, si el método isAlive() devuelve false, sabemos que estamos lidiando con un hilo Nuevo o Muerto. Si nos devuelve true, sabemos que el hilo se encuentra en estado Ejecutable o Suspendido. No se puede distinguir entre Nuevo y Muerto, ni entre un hilo Ejecutable o Parado.
10.4 Comunicación entre hilos Otra clave para el éxito y la ventaja de la utilización de múltiples hilos en una aplicación, es que pueden comunicarse entre sí. Se pueden diseñar hilos para utilizar objetos comunes, que cada hilo puede manipular independientemente de los demás. El ejemplo clásico de comunicación de hilos es el modelo productor / consumidor . Un hilo produce una salida que otro hilo consume. Vamos entonces a crear un productor, que será un hilo que irá generando letras mayúsculas a cierta velocidad; programaremos también un consumidor que irá consumiendo los caracteres que genere el
88 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
productor, y un monitor que controlará el proceso de sincronización entre los hilos. Funcionará como una estructura de cola: el productor inserta caracteres en un extremo y el consumidor los lee en el otro, y el monitor es en sí la cola. El comportamiento de nuestra aplicación se puede apreciar en el esquema de la Figura 33. Monitor c
+ depositar(char c)
+ recoger( ) : char
c
A T G U F S U
búfer Figura 33. Esquema productor - consumidor
El productor implementará la interfaz Runnable y su código es el siguiente: public class Productor implements Runnable { private Monitor monitor; private String alfabeto = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; private int retardo, no_letras; public Productor(Monitor monitor, int retardo, int no_letras) { // guarda la referencia del monitor para realizar la accion 'depositar' this.monitor = monitor; // guarda el retardo que va a sufrir el consumidor despues de cada consumo this.retardo = retardo; // guarda el numero de letras que se van a Producir this.no_letras = no_letras; } public void run() { // Ciclo para generar y depositar letras mayusculas en la cola del monitor for(int i = 0; i < no_letras; i ++) { // produce un byte con un valor aleatorio entre 65 y 90 byte b = (byte) (65 + Math.random() * 26); // deposita el byte creado en la cola del monitor monitor.depositar(b); // imprime el mensaje de la accion realizada System.out.println("Se deposito " + (char) b + " en la cola" ); // genera un retardo antes de producir más letras mayusculas try { Thread.sleep(retardo); } catch( InterruptedException e ) { } // fin try } // fin for() } // fin run()
Veamos ahora el código del consumidor, que también implementará la interfaz Runnable public class Consumidor implements Runnable { private Monitor monitor; private int retardo, no_letras; public Consumidor(Monitor monitor, int retardo, int no_letras) { // guarda la referencia del monitor para realizar la accion 'depositar' this.monitor = monitor; // guarda el retardo que va a sufrir el consumidor despues de cada consumo this.retardo = retardo; // guarda el numero de letras que se van a consumir this.no_letras = no_letras;
CAPÍTULO 10: HILOS 89 } public void run() { int i = 0; // Ciclo para consumir las letras de la cola del monitor while(i < no_letras) { // Obtiene el primer byte de la cola del monitor byte b = monitor.recoger( ); // Si el metodo devuelve 0, entonces la cola esta vacia, // en caso contrario, imprimir accion realizada e incrementar contador if(b != 0) { System.out.println("Se consumio el caracter " + (char) b); i ++; } // Generar un retardo antes de recoger más letras try { Thread.sleep(retardo); } catch( InterruptedException e ) { } } } }
Una vez visto lo que realizan el consumidor y el productor, pasamos a analizar al monitor. Lo que realiza la clase Monitor es una función de supervisión de las transacciones entre los dos hilos: el productor y el consumidor. Los monitores, en general, son piezas muy importantes de las aplicaciones multi-hilo porque mantienen el flujo de comunicación entre los hilos. El análisis y ejecución de la clase Monitor se deja al lector. Nótese que se ha implementado la cola utilizando la vectores, además de que se utilizan métodos synchronized para evitar accesos concurrentes a la cola. import java.util.*; public class Monitor { Vector cola = new Vector(); synchronized void imprimeVector() { for(int i = 0; i < cola.size(); i ++) { Byte b = (Byte) cola.get(i); System.out.print((char) b.byteValue()); } System.out.println(); } public synchronized void depositar(byte b) { cola.addElement(new Byte(b)); imprimeVector(); } public synchronized byte recoger() { if(cola.isEmpty()) return 0; Byte b = (Byte) cola.remove(0); imprimeVector(); return b.byteValue(); } public static void main(String[] a) { Monitor m = new Monitor(); int letrasPorConsumir = 20; (new Productor(m, 50, 20)); Thread t = new Thread t.start(); (new Consumidor(m, 100, 20)); t = new Thread t.start(); } // fin main } // fin clase
CAPÍTULO 11
INTERFAZ GRÁFICA DE USUARIO
La interfaz de usuario es la parte de una aplicación que permite a ésta interactuar con el usuario. Las interfaces de usuario pueden adoptar muchas formas que van desde la línea de comandos simple hasta las interfaces visuales avanzadas que proporcionan las aplicaciones actuales.Una aplicación sin una interfaz amigable impide que los usuarios obtengan el máximo rendimiento del programa. Java proporciona los elementos básicos para construir de manera sencilla interfaces de usuario aceptables a través del AWT, y opciones para mejorarlas mediante los componentes Swing.
11.1 AWT AWT es el acrónimo de Abstract Window Toolkit y representa una librería de clases Java para el
desarrollo de interfaces gráficas de usuario. La primera versión del AWT se desarrolló en sólo dos meses y, aunque se ha retocado posteriormente, es la parte más débil de todo lo que representa Java como lenguaje. El entorno que ofrece es simple, y es tal vez debido a la presión de los desarrolladores de Java de tener que lanzar algo al mercado que proporcione herramientas gráficas. Debido a la pobreza que mostró la primera versión del AWT, JavaSoft se unió con Netscape, IBM y Lighthouse Design para crear un conunto de clases que proporcionen una sensación visual agradable al usuario, que sean más fáciles de utilizar por el programador y que sean fácilmente transportables entre plataformas. Esta colección de clases se denominaron Java Foundation Classes (JFC), que en la plataforma Java 2 (JDK 1.2 en adelante) están constituidas por cinco grupos de clases: AWT mejorado, Accessibility, Java 2D, Drag & Drop y Swing. AWT fue diseñado pensando en que el programador no tuviese que preocuparse de detalles como controlar el movimiento del ratón o leer el teclado, ni tampoco de detalles como la escritura en pantalla. El AWT constituye una librería de clases orientada a objetos para cubrir estos recursos y servicios de bajo nivel. En términos generales, la librería AWT está constituido por: • • •
Componentes propios de la interfaz de usuario (botones, ventanas, cuadros de texto) Un modelo robusto para el manejo de eventos (teclado, ratón) Herramientas para imágenes, gráficos, tipos de letras
92 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
• •
Administradores de la disposición de componentes en un contenedor sin depender de un tamaño de ventana o resolución de la pantalla particular Clases para transferencia de datos (cortar y pegar ) a través del portapapeles nativo de la plataforma
Los componentes AWT estándares delegan en los peers la mayor parte de su funcionalidad. Los peers u observadores son componentes nativos del GUI del sistema operativo en donde se ejecutan y son manipulados por los componentes AWT. Los peers no están escritos en Java necesariamente. Cada componente AWT tiene un peer nativo equivalente. Cuando una aplicación Java solicita la creación y despliegue de un componente AWT (menú, ventana, botón), la JVM realmente crea y despliega un peer de la plataforma actual. Es por esta razón que los componentes AWT son considerados pesados, o heavyweight .
11.2 Swing Swing es también una librería de clases para el diseño de interfaces gráficas de usuario y representa un gran paso adelante respecto al AWT. Ahora los componentes visuales son beans (componentes especializados reutilizables, orientados a objetos) y utilizan un nuevo modelo para el manejo de eventos (teclado, ratón). Todos los componentes Swing son ligeros o lightweight porque ya no se usan componentes peer dependientes del sistema operativo, además Swing está totalmente escrito en Java. Las principales características de Swing se resumen a continuación:: • • • • •
Ofrece todas las facilidades visuales y manejos de eventos del AWT Comprende una versión de cada componente AWT pero desarrollado 100% en Java puro (JButton, JLabel, JPanel) Incoropora un conjunto de componentes de alto nivel (List Box, Tree View, Tabbed Panes) El diseño es en Java puro, sin dependencia en los peers Pluggable look & feel : la apariencia de una aplicación se adapta dinámicamente al sistema operativo y plataforma en que esté corriendo
Debido a las ventajas que ofrece Swing sobre AWT, en este capítulo dejaremos los componentes AWT a un lado y nos concentraremos en s componentes más importantes que ofrece el paquete Swing de Java para desarrollar aplicaciones visuales. El paso de AWT a Swing es muy sencillo y no hay que descartar nada de lo que se haya hecho con el AWT. Afortunadamente, los desarrolladores de Swing han considerado que muchos programadores de aplicaciones gráficas en Java se han acostumbrado a los nombres de las clases contenidas en el paquete AWT; por lo anterior, decidieron que cada uno de los componentes existentes en AWT tendrá su similar en Swing, pero se añadirá una “J” al inicio del nombre del componente Swing. Ejemplo: Button pertenece a AWT, JButton pertenece a Swing; los 2 ofrecen el mismo uso en las aplicaciones visuales, pero el primero tiene gran dependencia de los peers de la plataforma donde se ejecuta, y el segundo está desarrollado en Java puro, es más transportable y ofrece más facilidades para el programador. Es importante mencionar que, aunque Swing sea una versión mejorada de AWT, no significa que esté encaminado a sustituirlo por completo, sino a extenderlo, porque la funcionalidad básica de Swing descansa sobre el AWT. Aunque algunos componentes de Swing se corresponden a componentes del
CAPÍTULO 11: INTERFAZ GRÁFICA DE USUARIO 93
AWT, existen muchas clases útiles que sólamente se encuentran en las librerías AWT y que no se han extendido en Swing.
11.3 Creación y configuración de los componentes visuales básicos de Swing En esta sección veremos algunos de los componentes visuales más comunes que proporciona Swing y estudiaremos su funcionalidad a través de ejemplos programados. Para cada componente sólamente se presentan los métodos que consideramos de mayor importancia. Si se desea mayor información de estos componentes, consultar la API de Java.
11.3.1 JLabel Un objeto JLabel o etiqueta corresponde a un área para desplegar texto de sólo lectura en una ventana; el texto abarca un sólo renglón; una etiqueta no responde a eventos de entrada, por lo mismo no puede recibir el enfoque del usuario. Para crear una etiqueta, especificamos el texto que va a desplegar inicialmente, de la siguiente manera: JLabel etiqueta = new JLabel(“Esto es una etiqueta”);
Para cambiar y obtener el texto desplegado se utilizan los métodos setText y getText , respectivamente: etiqueta.setText(“Cambio de texto”); String texto = etiqueta.getText();
Para especificar los márgenes horizontales y verticales, utilizamos los siguientes métodos: o setHorizontalAlignment (int alineacion)
-
JLabel.CENTER JLabel.LEFT JLabel.RIGHT Valores posibles para o setVerticalAlignment (int alineacion) alineacion - JLabel.CENTER - JLabel.BOTTOM - JLabel.TOP Finalmente, para establecer los colores de fondo y del texto de una etiqueta, uilizamos. o setBackground (java.awt.Color o
color) setForeground (java.awt.Color color)
11.3.2 JTextField Comúnmente llamando cuadro de texto, JTextField es un componente que nos permite el despliegue y edición de una línea de texto. Este componente sí puede recibir un enfoque del usuario, es decir, a diferencia de una etiqueta, el cursor sí se puede colocar dentro del cuadro de texto. Para crear un cuadro de texto debemos especificar, ya sea el texto que va a desplegar inicialmente, la anchura del componente medido en columnas visibles, o ambos. JTextField cuadroTexto = new JTextField (“Texto inicial”);
94 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA JTextField cuadroTexto = new JTextField (15); JTextField cuadroTexto = new JTextField (“Texto inicial”; 15);
Para cambiar y obtener el texto desplegado se utilizan los métodos setText y getText , como en JLabel: cuadroTexto.setText(“Cambio de texto”); String texto = cuadroTexto.getText();
Para especificar el margen horizontal y los colores de fondo y texto utilizamos los mismos métodos que en el componente JLabel. Para deshabilitar el cuadro de texto e impedir que se modifique su contenido se utiliza el método setEnabled con false como parámetro; para habilitarlo otra vez, se utiliza true.
11.3.3 JButton Esta clase crea un botón etiquetado o con una imagen asignada. Un botón puede ser oprimido por el usuario para ocasionar que se efectúen acciones específicas. Para crear un botón indicamos el texto o la imagen que desplegará inicialmente de la siguiente manera: JButton cancelar = new JButton("Cancelar”); JButton guardar = new JButton(new ImageIcon("imagen1.gif"));
La clase ImageIcon nos permite obtener una imagen gif a partir de un URL o directorio especificado en su constructor, y agregarla a un botón. Esta clase se encuentra en el paquete javax.swing. Para habilitar/deshabilitar un botón utilizamos el método setEnabled de la misma manera que con el componente JTextField. if(status = EDICION) guardar.setEnabled (true); else guardar.setEnabled (false);
Para especificar el texto de ayuda emergente que se va a desplegar cuando se pase el puntero del ratón por encima del botón se utiliza el setToolTipText de la siguiente manera: guardar.setToolTipText(“Guardar los datos”);
11.3.4 JList Componente que le permite al usuario elegir uno o más objetos de una lista visual. El componente JList deberá tener asignado una estructura de datos que almacene todos los objetos que la lista desplegará. Si la cantidad de objetos a desplegar es fija, podemos usar un arreglo; en caso contrario, un Vector sería la mejor opción. El enlace entre el componente y la estructura de almacenamiento se puede realizar con el método setListData o en la construcción de la lista. String[] datos1 = {“Objeto 1”, “Objeto 2”, “Objeto 3”, “Objeto 4”}; datos2 = new Vector(); Vector JList lista1 = new JList(); lista1.setListData(datos1);
ó
JList lista1 = new JList(datos1);
JList lista2 = new JList(); lista2.setListData(datos2);
ó
JList lista2 = new JList(datos2);
CAPÍTULO 11: INTERFAZ GRÁFICA DE USUARIO 95
Cuando realicemos cambios en la fuente de datos, como agregar, eliminar o modificar elementos del vector o arreglo enlazado, es necesario actualizar la lista de manera que se desplieguen los datos actualizados. Esto lo podemos realizar invocando el método repaint de JList o cambiando la selección de la lista con el método setSelectedIndex . A continuación se listan los métodos que definen la apariencia visual de una lista: o setBackground (java.awt.Color o o o
o o
color)
color de fondo de la lista setForeground (java.awt.Color color) color del texto de los elementos de la lista setSelectionBackground (java.awt.Color color) color de fondo del(los) elemento(s) seleccionado(s) setSelectionForeground (java.awt.Color color) color del texto del(los) elemento(s) seleccionado(s) setFixedCellHeight (int altoCelda) alto de cada celda en pixeles setFixedCellWidth(int anchoCelda) ancho de cada celda en pixeles
Los siguientes métodos de JList se relacionan con la selección de elementos: (int indice) selecciona el elemento número indice de la lista, donde el 1er elemento tiene indice = 0; la ausencia de selección se representa con indice = –1; cualquier indice < –1 será inválido getSelectedIndex ( ) : int devuelve el índice del elemento seleccionado de la lista getSelectedValue ( ) : Object devuelve una referencia del elemento seleccionado; implica la presencia de un cast utilizando el tipo de dato que tengan los elementos de la lista clearSelection ( ) despues de invocar este método, ningún elemento de la lista estará seleccionado setSelectionMode (int modo) establece el modo de selección de la lista, donde modo puede tomar uno de los siguientes valores: - ListSelectionModel.SINGLE_SELECTION : nada más se puede seleccionar un elemento a la vez - ListSelectionModel.SINGLE_INTERVAL_SELECTION : nos permite seleccionar uno o más elementos contiguos (utilizando shift y el botón izquierdo del mouse) - ListSelectionModel.MULTIPLE_INTERVAL_SELECTION : nos permite seleccionar uno o más elementos contiguos o separados (utilizando ctrl y el botón izquierdo del mouse) setSelectedIndices (int[] indices) selecciona uno o más elementos de la lista, dado por un arreglo de enteros getSelectionIndices (int modo) devuelve un arreglo de enteros indicando los elementos de la lista que están seleccionados getValueIsAdjusting ( ) : boolean devuelve true si el valor de la lista está en un proceso de ajuste como resultado de la interacción con el usuario; validar que el valor devuelto de este método sea true nos garantiza que no suceda nada hasta que se haya completado el cambio de selección
o setSelectedIndex
o o
o o
o o
o
96 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
11.3.5 JScrollPane La clase JScrollPane es un componente y a la vez contenedor de componentes Swing; nos proporciona barras de desplazamiento vertical y horizontal para poder visualizar el componente contenido en los casos en que el espacio visual asignado no es suficiente para desplegar toda la información. El constructor de JScrollPane recibe como parámetro la referencia de un componente. Podemos utilizar este componente para poder visualizar todos los elementos de una lista; cuando el total de elementos agregados haya rebasado las dimensiones de la lista, aparecerá una barra de desplazamiento vertical que nos permitirá navegar sobre la lista para ver los demás elementos; si el texto de algún elemento de la lista rebasa la anchura de la lista, deberá aparecer la barra horizontal. Vector datos = new Vector(); JList lista = new JList(datos); JScrollPane scroll = new JScrollPane(lista);
11.3.6 JComboBox También llamado cuadro combinado, JComboBox es un componente de uso semejante al JList que combina un botón y una lista. Cuando el cuadro combinado no está en uso, sólamente despliega el elemento seleccionado de la lista. Al presionar el botón aparece la lista desplegando n elementos a partir del seleccionado. El valor de n representa la cantidad máxima de elementos que podrán desplegarse al mismo tiempo. Cuando la cantidad de elementos contenidos en el cuadro combinado rebasa el valor de n, aparece una barra de desplazamiento vertical. Para crear un cuadro combinado con datos iniciales hacemos lo mismo que con el JList: mandar como parámetro del constructor un vector o un arreglo. También podemos crear un cuadro combinado vacío. String[] datos1 = {“Objeto 1”, “Objeto 2”, “Objeto 3”, “Objeto 4”}; datos2 = new Vector(); Vector JComboBox combo0 = new JComboBox(); JComboBox combo1 = new JComboBox(datos1); JComboBox combo2 = new JComboBox(datos2);
A diferencia del JList, el JComboBox nos permite editar elementos a partir de métodos propios, sin tener que realizar tales acciones sobre el vector o el arreglo asignado inicialmente. o addItem (Object objeto)
agrega el elemento objeto al final del cuadro combinado: combo0.addItem (“Objeto 1”);
o insertItemAt (Object objeto, int indice)
o
agrega el elemento objeto en el lugar indice de la lista; los elementos con índice recorren un lugar al final removeItemAt (int indice) elimina el elemento número indice del cuadro combinado:
indice se
combo1.removeItemAt(2); // elimina “Objeto 3” o
getItemAt (int indice) : Object
devuelve el elemento de la lista situado en el lugar indice , como un objeto. String s = (String) combo1.getItemAt(1); // devuelve “Objeto 2”
o getSelectedIndex ( ) : int
devuelve el índice del elemento seleccionado, o -1 si no hay ningún elemento seleccionado
CAPÍTULO 11: INTERFAZ GRÁFICA DE USUARIO 97 o setSelectedIndex (int indice) o
selecciona el elemento dado por indice getItemCount ( ): int devuelve el número de elementos de la lista
11.3.7 Cuadros de diálogo Una de las formas más comunes de desplegar cuadros de diálogo informativos es utilizando el método estático showMessageDialog de la clase JOptionPane. El cuadro de diálogo que se presenta despliega un ícono de acuerdo al tipo de mensaje a mostrar: error, aviso, información, pregunta. Además, cuando el cuadro de diálogo está visible, todas las ventanas que forman parte de la aplicación se deshabilitan. o showMessageDialog (Component
padre, Object mensaje, String titulo, int tipoMensaje) - padre: el componente que servirá como referencia para centrar el cuadro de diálogo; los componentes padres más comunes son: JPanel, JFrame. Utilizamos this si el componente padre es la referencia de la clase donde fue invocada el cuadro de diálogo, y null si no tiene componente padre, y con ello, la referencia posicional será la pantalla misma. - mensaje : la información que se va a desplegar en el cuadro de diálogo - titulo: la información que va en la barra de titulo - tipoMensaje : existe un icono asignado para cada tipo de mensaje; los tipos de mensaje disponibles están dados por los siguientes atributos estáticos de JOptionPane: ERROR_MESSAGE, WARNING_MESSAGE, INFORMATION_MESSAGE,, QUESTION_MESSAGE, PLAIN_MESSAGE
ejemplo: public class Calculadora extends JFrame { … public void dividir(int numerador, int denominador){ if(denominador == 0) JOptionPane.showMessageDialog(this, "Division entre cero", “Error de datos”, JOptionPane.ERROR_MESSAGE); … } }
En el ejemplo anterior, un objeto Calculadora realmente es una ventana porque su clase extiende a JFrame. El primer parámetro del método showMessageDialog (this) se refiere al objeto Calculadora actual, por lo que, al desplegarse el cuadro de diálogo, la ventana se deshabilitará y al cerrarse el cuadro de diálogo se volverá a habilitar.
11.4 Adición de los componentes visuales básicos de Swing en una aplicación Toda aplicación visual en Java se podrá desplegar utilizando uno de dos medios visuales posibles: a través de un applet en una página de internet, o dentro de una ventana.
11.4.1 JFrame Swing proporciona el componente JFrame que nos permite crear ventanas en pocos pasos. Dentro de un JFrame podemos agregar cualquier otro componente visual de Swing para conformar la GUI de nuestra
98 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA
aplicación. Para crear una ventana en Java es suficiente con instanciar o extender la clase JFrame. La segunda técnica es la más recomendada por cuestiones de diseño orientado a objetos, y porque nos evita la escritura repetitiva del objeto de tipo JFrame al invocar sus métodos. La clase JFrame se encuentra en el paquete javaw.swing. La siguiente clase crea y despliega una ventana utilizando la 1ª técnica: import javax.swing.*; public class Ventana1 { public Ventana1() { JFrame ventana1 = new JFrame(“Mi primer ventana”); ventana1.setSize(300, 200); ventana1.setResizable(false); ventana1.setDefaultCloseOperation(3); ventana1.setVisible(true); } public static void main(String[] args) { new Ventana1(); } }
Asi quedaría la clase anterior utilizando la 2ª técnica: herencia . import javax.swing.*; public class Ventana1 extends JFrame { public Ventana1() { super(“Mi primer ventana”); setSize(300, 200); setResizable(false); setDefaultCloseOperation(3); setVisible(true); } public static void main(String[] args) { new Ventana1(); } }
Analicemos cada una de las líneas del constructor de Ventana1 . Uno de los constructores que ofrece la clase JFrame espera como parámetro de entrada una cadena de texto que representa el título de la ventana, el cual aparecerá en la barra de la parte superior de la misma. Por esta razón, utilizamos la sentencia super para enviar el título de nuestra ventana a la clase base en el momento de su creación. Otra forma de asignar el título a la ventana es utilizando el método setTitle de JFrame. El método setSize asigna el tamaño de nuestra ventana mediante dos valores enteros que representan sus dimensiones de anchura y altura en pixeles. El método setResizable indica que no podemos estirar o encoger la ventana si el parámetro; setDefaultCloseOperation establece la acción que se llevará a cabo cuando el usuario intente cerrar la ventana: 0 = no realiza acción, 1 = se oculta, 2 = se libera el espacio de memoria, 3 = termina la aplicación. Una vez que se hayan especificado todos los parámetros de la ventana y agregado todos los componentes dentro de la misma, se indica al sistema que despliegue la ventana en la pantalla mediante el método setVisible y enviando como parámetro true. Para un buen desempeño de nuestra aplicación, este método deberá ser el último que se invoque. No podemos colorear un objeto JFrame ni agregar componentes directamente dentro de él; esto sólamente podemos realizarlo utilizando contenedores (Container). Todo JFrame tiene un contenedor
CAPÍTULO 11: INTERFAZ GRÁFICA DE USUARIO 99
de manera implícita, y es a través de él que podemos agregar componentes a nuestra ventana. Este contenedor abarca todo el ancho de la ventana excluyendo los bordes y la barra de título. Para obtener el contenedor que tiene todo objeto JFrame utilizamos el método getContentPane como se verá en la siguiente sección.
11.4.2 JPanel La Clase JPanel hereda de la clase Container, lo que ocasiona que un objeto JPanel o pánel tenga todas las características de un contenedor, con la particularidad de que puede contener componentes Swing, como los que veremos en las secciones siguientes. Un pánel también puede contener más paneles, y éstos a su vez pueden contener aún más páneles. Un pánel nos sirve también como lienzo para dibujar figuras geométricas. A través de un objeto JPanel podemos agregar componentes visuales a una ventana. La siguiente clase amplía la funcionalidad de la clase Ventana1 definida en la sección anterior. Se obtiene el contenedor que tiene todo JFrame utilizando el método getContentPane ( ), se convierte en un objeto de tipo JPanel, se le asigna el color naranja de fondo y un borde amarillo. import java.awt.Color; import javax.swing.*; import javax.swing. border.*; public class Ventana1 extends JFrame { public Ventana1() { super(“Mi primer ventana”); setSize(300, 200); setResizable(false); setDefaultCloseOperation(3); JPanel panel = (JPanel) getContentPane(); panel.setBackground (Color.ORANGE); panel.setBorder(new LineBorder(Color.YELLOW )); setVisible(true); } public static void main(String[] args) { new Ventana1(); } }
El método setBackground establece el color de fondo de un panel; en Java, establecemos un color utilizando un objeto de la clase Color, contenida en el paquete java.awt . Esta clase tiene una lista de atributos finales estáticos que nos sirven para invocar los colores más comunes. En el ejemplo, se colorea el pánel de color naranja. Para definir colores personalizados, creamos un objeto de tipo Color y especificamos la cantidad de rojo, verde y azul que deseemos, de la siguiente manera: //
rojo, verde, azul
Color cafe = new Color(170, 130, 100);
El valor para cada componente de nuestro color RGB puede ir de 0 hasta 255, donde (0, 0, 0) equivale al color negro y (255, 255, 255) al color blanco. El método setBorder nos permite especificar el borde de un pánel; en el ejemplo se establece un borde lineal de color amarillo. A continuación se listan los constructores para los tipos de bordes más comunes que proporciona Swing en la librería javax.swing.border :
100 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA o o o
o
new EtchedBorder( ) new LineBorder(java.awt.Color) new BevelBorder(int borde) - BevelBorder.RAISED - BevelBorder.LOWERED new SoftBevelBorder(int borde) - SoftBevelBorder.RAISED - SoftBevelBorder.LOWERED
Valores posibles para borde
11.4.3 Disposición de componentes Swing en un JPanel La clase JPanel proporciona el método add que nos permite agregar componentes AWT o Swing dentro de un pánel. Debido a la portabilidad que tienen las aplicaciones Java, no es posible especificar coordenadas absolutas para la ubicación de componentes dentro de un pánel; esto es porque estaríamos casando nuestra aplicación con algún sistema de ventanas específico. Para resolver este problema, JPanel proporciona diferentes formas de disponer los componentes La forma de ubicar los componentes en un JPanel se especifica con el método setLayout , el cual recibe como parámetro un objeto perteneciente a una clase que implemente la interfaz LayoutManager o LayoutManager2. Alguna de estas clases implementadoras se presentan a continuación. Todas pertenecen al paquete java.awt .
11.4.3.1 FlowLayout Es la disposición o layout por omisión de un JPanel. FlowLayout acomoda los componentes en el pánel siguiendo un flujo similar a la escritura de texto en un párrafo: de izquierda a derecha y de arriba hacia abajo. Los componentes se agregan siguiendo una línea horizontal; cuando algún componente no tenga espacio suficiente para desplegarse, se avanza a la siguiente línea o renglón. Cada línea está centrada. Para especificar el espacio horizontal y vertical en pixeles existente entre cada componente, utilizamos los métodos setHgap y setVgap, respectivamente, como se observa en el ejemplo siguiente: import javax.swing.*; import javax.swing.border.*; import java.awt.*; public class Disposicion1 extends JFrame { public Disposicion1() { super("Disposición en FlowLayout"); setSize(300, 150); setResizable(false); setDefaultCloseOperation(3); FlowLayout flow = new FlowLayout(); flow.setHgap(10); flow.setVgap(25); JPanel panel = (JPanel) getContentPane(); panel.setBackground(new Color(100, 160, 190)); panel.setBorder(new BevelBorder(BevelBorder.LOWERED)); panel.setLayout(flow); panel.add (new JButton("boton 1")); panel.add (new JButton("boton 2")); panel.add (new JButton("boton 3"));
CAPÍTULO 11: INTERFAZ GRÁFICA DE USUARIO 101 panel.add (new JLabel("etiqueta 1:")); panel.add (new JTextField("texto 1", 10)); panel.add (new JButton("boton 4")); setVisible(true); } public static void main(String[] args) { new Disposicion1(); } }
La ejecución de la clase Disposicion1 en el sistema operativo Windows XP, desplegará la siguiente ventana:
Figura 34. Disposición de componentes en FlowLayout
11.4.3.2 GridLayout Este tipo de disposición despliega los componentes en una cuadrícula. El panel se divide en rectángulos del mismo tamaño, y cada componente se coloca en un rectángulo; por esta razón, todos los componentes tendrán las mismas dimensiones. La cantidad de filas y columnas en que se dividirá el panel se especifica comúnmente en el constructor de GridLayout, como se observa en el ejemplo siguiente: import javax.swing.*; import javax.swing.border.*; import java.awt.*; public class Disposicion2 extends JFrame { public Disposicion2() { super("Disposición en GridLayout"); setSize(300, 150); setResizable(false); setDefaultCloseOperation(3); // (filas, columnas) GridLayout grid = new GridLayout(3, 2); grid.setHgap(10); grid.setVgap(15); JLabel etiqueta = new JLabel("etiqueta 1:"); etiqueta.setHorizontalAlignment(JLabel.RIGHT); JPanel panel = (JPanel) getContentPane(); panel.setBackground(Color.orange); panel.setBorder(new LineBorder(Color.red)); panel.setLayout(grid); panel.add (new JButton("boton 1"));
102 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA panel.add (new JButton("boton 2")); panel.add (new JButton("boton 3")); panel.add (new JButton("boton 4")); panel.add (etiqueta); panel.add (new JTextField("texto 1", 10)); setVisible(true); } public static void main(String[] args) { new Disposicion2(); } }
La ejecución de la clase Disposicion2 en el sistema operativo Windows XP, desplegará la siguiente ventana:
Figura 35. Disposición de componentes en GridLayout
11.4.3.3 BorderLayout Este tipo de disposición acomoda los componentes en 5 regiones diferentes: norte (arriba), sur (abajo), centro, este (derecha), oeste (izquierda). Cada región está identificada con una constante perteneciente a la clase BorderLayout: NORTH, SOUTH, CENTER, EAST, WEST. Para agregar componentes a un pánel teniendo como disposición BorderLayout , se utiliza el método add de JPanel con 2 parámetros: componente y región. Si se omite la región, se colocará el componente en el centro. La siguiente clase acomoda 5 botones en una ventana utilizando BorderLayout. import javax.swing.*; import javax.swing.border.*; import java.awt.*; public class Disposicion3 extends JFrame { public Disposicion3() { super("Disposición en BorderLayout"); setSize(300, 150); setResizable(false); setDefaultCloseOperation(3); JPanel panel = (JPanel) getContentPane(); panel.setBackground(new Color(180, 200, 130)); panel.setBorder(new SoftBevelBorder(SoftBevelBorder.RAISED)); BorderLayout grid = new BorderLayout(); grid.setHgap(5); grid.setVgap(10);
CAPÍTULO 11: INTERFAZ GRÁFICA DE USUARIO 103 panel.setLayout(grid); panel.add (new JButton("Norte"), panel.add (new JButton("Centro"), panel.add (new JButton("Sur"), panel.add (new JButton("Oeste"), panel.add (new JButton("Este"), setVisible(true);
BorderLayout.NORTH); ); BorderLayout.CENTER BorderLayout.SOUTH); BorderLayout.WEST); BorderLayout.EAST);
} public static void main(String[] args) { new Disposicion3(); } }
La ejecución de la clase Disposicion3 en el sistema operativo Windows XP, desplegará la siguiente ventana:
Figura 36. Disposición de componentes en BorderLayout
Existen más formas de acomodar componentes en un pánel. Para conocer otras clases para la disposición de componentes consultar las interfaces LayoutManager y LayoutManager2 en la API de Java. Si agregamos páneles dentro de páneles y asignamos a cada uno layouts diferentes, podemos acercarnos al acomodo de componentes más deseado.
11.5 Manejo de eventos con componentes Swing En esta sección aprenderemos a controlar los eventos que desencadena Java cada vez que se pulsa un botón o se cambia la selección de una lista o cuadro combinado.
11.5.1 JButton Si de desea detectar que un botón se ha presionado, es necesario asignarle un objeto listener , esto es, aquel que está siempre “escuchando ” o “al tanto de” los cambios sucedidos con el botón. De esta manera, cuando un usuario presione el botón, el listener deberá reaccionar y avisar a la aplicación de tal evento para que ésta responda con una serie de acciones. Para tales propósitos, Java proporciona la interfaz ActionListener que pertenece al paquete java.awt.event . La clase que esté interesada en procesar los eventos desencadenados al pulsar un botón, deberá implementar esta interfaz, incluyendo su único método: actionPerformed(ActionEvent e) . Cuando se presiona un botón que esté siendo escuchado por un objeto ActionListener, se crea un objeto de tipo ActionEvent que contiene información del evento, y enseguida se ejecuta el método
104 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA actionPerformed . Todo lo que queramos que realice la aplicación después de la pulsación del botón
deberá estar contenida en tal método. La siguiente clase implementa la interfaz ActionListener, por lo tanto, tendrá la cualidad de poder escuchar las pulsaciones que sucedan en algún botón. Cuando se presione algún botón asociado, aparecerá un cuadro de diálogo desplegando la cadena de texto recibida en el consructor de la clase. import javax.swing.*; import java.awt.event.*; public void DesplegarBoton implements ActionListener { private String nombreBoton; private JFrame parent; public DesplegarMensaje(String nb, JFrame parent) { nombreBoton = nb; this.parent = parent; } ( public void actionPerformed ActionEvent e) { JOptionPane.showMessageDialog( parent, nombreBoton, “Botón presionado”, JOptionPane.INFORMATION_MESSAGE); } }
Para asociar la clase DesplegarBoton con algún botón, invocamos el método addActionListener de la clase JButton, a partir del(os) botón(es) que deseen ser escuchados, y enviamos como parámetro una instancia de DesplegarBoton . Así, esta instancia reaccionará cada vez que se realicen pulsaciones sobre los botones asociados. Modificamos la clase Disposicion3 de tal manera que cada vez que se pulse un botón, se despliegue un cuadro de diálogo con el nombre del botón pulsado. import javax.swing.*; import javax.swing.border.*; import java.awt.*; public class Disposicion3 extends JFrame { JButton botonNorte = new JButton("Norte"); JButton botonCentro = new JButton("Centro"); JButton botonSur = new JButton("Sur"); JButton botonOeste = new JButton("Oeste"); JButton botonEste = new JButton("Este"); public Disposicion3() { super("Disposición en BorderLayout"); setSize(300, 150); setResizable(false); setDefaultCloseOperation(3); BorderLayout grid = new BorderLayout(); panel.setBackground(new Color(180, 200, 130)); grid.setHgap(5); grid.setVgap(10); JPanel panel = (JPanel) getContentPane(); panel.setBorder(new SoftBevelBorder(SoftBevelBorder.RAISED));
CAPÍTULO 11: INTERFAZ GRÁFICA DE USUARIO 105 panel.setLayout(grid); botonNorte.addActionListener (new botonCentro.addActionListener(new botonSur.addActionListener (new botonOeste.addActionListener (new botonEste.addActionListener (new panel.add(botonNorte , panel.add(botonCentro, panel.add(botonSur , panel.add(botonOeste , panel.add(botonEste ,
DesplegarBoton("Norte" , DesplegarBoton("Centro", DesplegarBoton("Sur" , , DesplegarBoton("Oeste" DesplegarBoton("Este" ,
this)); this)); this)); this)); this));
BorderLayout.NORTH); BorderLayout.CENTER); BorderLayout.SOUTH); BorderLayout.WEST); BorderLayout.EAST);
setVisible(true); } public static void main(String[] args) { new Disposicion3(); } }
La ejecución de la clase Disposicion3 desplegará lo siguiente al pulsar el botón Norte:
Figura 37. Efecto de la pulsación de un botón
11.5.2 JList Si se desea realizar alguna acción cada vez que el usuario seleccione un elemento diferente de un cuadro de lista, es necesario asociar a la lista un objeto listener para que esté al tanto de los cambios en la selección de sus elementos. Para tales propósitos, Java proporciona la interfaz ListSelectionListener que pertenece al paquete javax.swing.event . Para que una clase pueda gestionar los cambios de selección de una lista, deberá implementar esta interfaz, incluyendo su único método: valueChanged(ListSelectionEvent e) . Cuando se selecciona un elemento diferente de una lista, ya sea por acción del usuario o de la aplicación, se crea un objeto de tipo ListSelectionEvent que contiene información del evento, y enseguida se ejecuta el método valueChanged . Todo lo que queramos que realice la aplicación después del cambio de selección de la lista deberá estar contenida en tal método. La siguiente clase implementa la interfaz ListSelectionListener , por lo tanto, tendrá la cualidad de poder escuchar los cambios que sucedan en alguna lista. Cuando se cambie la selección de la lista asociada, aparecerá un cuadro de diálogo desplegando el elemento seleccionado.
106 PROGRAMACIÓN ORIENTADA A OBJETOS EN JAVA import javax.swing.*; import javax.swing.event.*; public class DesplegarElemento implements ListSelectionListener { private JFrame parent; private JList lista; public DesplegarElemento(JFrame parent, JList lista) { this.parent = parent; this.lista = lista; } public void valueChanged (ListSelectionEvent e) { if(lista.getValueIsAdjusting()) return; String elemento = (String) lista.getSelectedValue(); JOptionPane.showMessageDialog(parent, elemento, "Elemento seleccionado", JOptionPane.PLAIN_MESSAGE); } }
Para
asociar
la
clase DesplegarElemento con alguna lista, invocamos el método addListSelectionListener de la clase JList, a partir del(as) lista(s) que deseen ser escuchadas, y enviamos como parámetro una instancia de DesplegarElemento. Así, esta instancia reaccionará cada vez que se realicen cambios de selección sobre las listas asociadas. La siguiente clase despliega un cuadro de diálogo con el nombre del elemento seleccionado de la lista, cada vez que se haga un cambio de selección. import javax.swing.*; import java.awt.*; import javax.swing.border.*; public class EventosLista extends JFrame { private final Color AZUL private String[] nombres
= new Color(150, 180, 200); = {"Abstraccion", "Modularidad", "Encapsulamiento", "Agregacion", "Herencia", "Polimorfismo"}; private JLabel titulo = new JLabel("Características de un LOO"); private JList lista = new JList(nombres); private JScrollPane scroll = new JScrollPane(lista); public EventosLista() { super("Captura de eventos de una lista"); setSize(320, 150); setResizable(false); setDefaultCloseOperation(3); BorderLayout grid = new BorderLayout(); grid.setVgap(5); titulo.setHorizontalAlignment(JLabel.CENTER); lista.setFixedCellWidth(100); lista.setFixedCellHeight(22); lista.setBackground(AZUL); lista.setForeground(Color.blue); lista.setSelectionBackground(Color.yellow); lista.setSelectionForeground(Color.red);
CAPÍTULO 11: INTERFAZ GRÁFICA DE USUARIO 107 lista.addListSelectionListener(new DesplegarElemento(this, lista)); JPanel panel = (JPanel) getContentPane(); panel.setLayout(grid); panel.setBackground(AZUL); panel.setBorder(new SoftBevelBorder(SoftBevelBorder.LOWERED)); panel.add(titulo, BorderLayout.NORTH); panel.add(scroll, BorderLayout.CENTER); setVisible(true); } public static void main(String[] args) { new EventosLista(); } }
La ejecución de la clase EventosLista desplegará lo siguiente al seleccionar el segundo elemento con el ratón o el teclado:
Figura 38. Efecto del cambio de selección de una lista