Conceptos de Programación Orientada a Objetos Héctor Adolfo Andrade Gómez
2013
Conceptos de Programación Orientada a Objetos
Derechos reservados: © 2013, Héctor Adolfo Andrade Gómez Obra registrada ante el Instituto Nacional del Derecho de Autor. Queda prohibida la reproducción o transmisión total o parcial del contenido de la presente obra en cualesquiera formas, sean electrónicas o mecánicas, sin el consentimiento previo y por escrito del autor.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
2
TABLA DE CONTENIDO Introducción _____________________________________________________________ 6 Objetivo del libro ____________________________ ________________________________________________ __________________________________ ______________ 7 Método de enseñanza. _____________________________________ _______________________________________________________ ____________________ __ 7 El lenguaje de programación. ___________________ ______________________________________ _________________________________ ______________ 8 Contenido del libro. __________________________ ______________________________________________ __________________________________ ______________ 8
1. Conceptos de Programación _____________________________________________ 10 Computadora. _____________________________________ ________________________________________________________ __________________________ _______ 10 Lenguajes de Programación. _________________________________ ___________________________________________________ ___________________ _ 13 13 Paradigmas de Lenguajes de Programación. ____________________________________ _______________________________________ ___ 13 Programación imperativa. _________________________ ____________________________________________ _____________________________ __________ 13 Programación Lógica ___________________ _______________________________________ ______________________________________ ___________________ _ 16 Programación Orientada a Objetos ____________________________________ ______________________________________________ __________ 17 Breve Historia de los Lenguajes de Programación. __________________________________ __________________________________ 19 Resumen del capítulo. ___________________________ _______________________________________________ ______________________________ __________ 19 Ejercicios y preguntas ______________________________________ ________________________________________________________ ___________________ _ 19
2. Introducción a la Programación Orientada Orientada a Objetos _________________________ 22 Programación Orientada a Objetos. ____________________________________ ______________________________________________ __________ 22 Programación Orientada a Objetos vs Programación Imperativa. ______________________ ______________________ 23 Abstracción. ____________________________________ _______________________________________________________ _____________________________ __________ 24 Ejemplo: Cálculo de la Nómina de una Empresa. ___________________ ____________________________________ _________________ 24 Clases y Objetos _______________________ ___________________________________________ ______________________________________ ___________________ _ 25 Creación de una clase en Java. _____________________________________ __________________________________________________ _____________ 26 Creación de objetos. ___________________ _______________________________________ ______________________________________ ___________________ _ 28 Uso de Paquetes. ____________________________ ________________________________________________ _________________________________ _____________ 32 Resumen del Capítulo. _____________________________________ _______________________________________________________ ___________________ _ 38 Ejercicios y preguntas. _____________________ _________________________________________ ____________________________________ ________________ 38
3. Conceptos Básicos de Programación Orientada Orientada a Objetos Objetos _____________________ 42
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
3
Métodos y Atributos de Clase. _____________________________________ __________________________________________________ _____________ 42 Sobrecarga de Constructores ___________________ ______________________________________ ________________________________ _____________ 48 Arreglos ____________________________________ ______________________________________________________ ________________________________ ______________ 50 Declaración y Creación de arreglos. ____________________________________ ______________________________________________ __________ 51 Permisos de Acceso a los Miembros de los Objetos. _____________________________ _________________________________ ____ 57 Setters y Getters ___________________ _______________________________________ ______________________________________ ______________________ ____ 61 Resumen del Capítulo. _____________________________________ _______________________________________________________ ___________________ _ 67 Ejercicios y preguntas. _____________________ _________________________________________ ____________________________________ ________________ 67
4. Herencia y Polimorfismo ________________________________________________ 74 Concepto de herencia.______________________________________ ________________________________________________________ ___________________ _ 75 La Herencia como mecanismo de reúso de código. __________________________________ __________________________________ 76 Permiso de Acceso Protegido ___________________ ______________________________________ ________________________________ _____________ 79 Sobre-escritura de Métodos. ___________________ ______________________________________ ________________________________ _____________ 86 Clases Abstractas. ______________________________________ ________________________________________________________ ______________________ ____ 88 Interfaces. _____________________________________ ________________________________________________________ _____________________________ __________ 94 Polimorfismo ___________________ ______________________________________ ______________________________________ _________________________ ______ 104 Retorno Covariante ____________________________________________________ __________________________________________________________ ______ 113 Clases Finales ____________________________________________ _______________________________________________________________ ___________________ 114 El Ejemplo de la Nómina Revisitado _______________________________________ _____________________________________________ ______ 115 Resumen del Capítulo ______________________________________ ________________________________________________________ __________________ 124 Ejercicios y preguntas. _____________________ _________________________________________ ___________________________________ _______________ 125
5. Excepciones Excepciones y Flujos Avanzados Avanzados de Datos ___________________________ _________________________________ ______ 132 Excepciones ____________________________________ _______________________________________________________ ____________________________ _________ 132 Manejo de Excepciones en Java. ____________________________________ ________________________________________________ ____________ 133 ________________________________________________ ___ 134 Cláusulas “Throws” y “Throw”. ______________________________________________ Cláusulas “try” y “catch”. ___________________ ______________________________________ __________________________________ _______________ 135 Otra forma de manejar las Excepciones __________________________ __________________________________________ ________________ 137 Propagación de Excepciones. ___________________ ______________________________________ _______________________________ ____________ 141 __________________________________________ ______________________________________________________ ____________ 144 Finalmente… Finally_ Flujos Avanzados de Datos._____________________________________ ____________________________________________________ _______________ 148
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
4
Flujos de Bytes __________________________________ _____________________________________________________ ____________________________ _________ 149 Flujos de Caracteres. ___________________ _______________________________________ ______________________________________ __________________ 151 Otros tipos de Flujos.___________________ _______________________________________ ______________________________________ __________________ 154 Serialización de Objetos ____________________________________ ______________________________________________________ __________________ 154 Serializando objetos que hacen referencia r eferencia a otros objetos. __________________________ 159 ___________________________________ __ 160 El Ejemplo de la Nomina Revisitado…Revisitado Revisitado…Revisitado _________________________________ Utilizando Serialización de Objetos ____________________________________ _____________________________________________ _________ 165 Resumen del Capítulo ______________________________________ ________________________________________________________ __________________ 170 Ejercicios y Preguntas. _____________________ _________________________________________ ___________________________________ _______________ 172
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
5
Introducción La programación de computadoras consiste en escribir código en algún lenguaje de programación para que una computadora realice realice una tarea específica. Esta actividad es considerada considerada por muchas personas como arte, para otros una ciencia, para otros una mera actividad económica, para algunos una actividad divertida, un pasatiempo. Sin embargo, para algunos, es una actividad tediosa, aburrida y muy difícil. Algo que es indiscutible, es que el desarrollo de programas (o de software como se le conoce comúnmente) es uno de los factores más importantes en la conformación de los grandes cambios económicos, sociales y políticos de los t iempos recientes. Se han creado grandes fortunas. Se han iniciado movimientos sociales, ¡Algunos de estos movimientos han llegado incluso a derrocar gobiernos! Se ha cambiado la forma en la que la gente se entretiene, comunica y aprende. Aunque es cierto que el software no es el único factor que ha provocado estos cambios, es un hecho que ha jugado un papel muy importante y seguramente lo seguirá haciendo en el futuro. Existen varias formas o paradigmas para escribir programas. Una de las más utilizadas en la industria y en la academia es la Programación Orientada a Objetos. La intención de este libro es que el estudiante construya su conocimiento sobre la Programación Orientada a Objetos sobre bases firmes, haciendo énfasis en los conceptos y no en la sintaxis del lenguaje de programación.
El libro es el producto de más de 25 años de experiencia en la enseñanza de programación, en los cuales, el autor ha notado que aunque muchos estudiantes escriben programas que resuelven problemas, no tienen firmes los conceptos de la programación e incluso no utilizan correctamente las características de los lenguajes orientados a objetos. Lo anterior provoca que los programas no cumplan con criterios deseables de mantenibilidad, eficiencia y robustez entre otros. Por lo anterior, el enfoque del libro es hacia los conceptos. Pues es mucho más importante que el estudiante comprenda y utilice adecuadamente los conceptos de la programación orientada a objetos a que entienda y memorice la sintaxis de un lenguaje o peor aún, que memorice soluciones completas a problemas. ¿A quién va dirigido este libro?
Este libro está dirigido primordialmente a personas que desean aprender a programar en un lenguaje orientado a objetos. Aunque los conceptos presentados en este libro se aplican al lenguaje Java, muchos de ellos son aplicables también a todos los lenguajes orientados a objetos. Requisitos
Se asume que el estudiante conoce algún lenguaje de programación estructurada, como el lenguaje “C” o Pascal. Pascal. Aunque no es necesario, es deseable que se tenga experiencia en el
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
6
desarrollo estructurado de algoritmos pues en este libro no se explica el funcionamiento de las estructuras de control ni la solución algorítmica algor ítmica de problemas. Objetivo del libro
Existen varios tipos de habilidades y conocimientos que un programador que utiliza el paradigma orientado a objetos debe tener. Primero, debe saber resolver problemas algorítmicamente. Esta habilidad está relacionada con la aplicación estructuras de datos simples y estructuras de control iterativas, secuenciales, de lectura/escritura, etc. Generalmente existen cursos y libros dedicados al desarrollo de este tipo de habilidades como son: Fundamentos de Programación, Lenguajes Algorítmicos, Introducción a la Programación, Programación, etc. En este libro, libro, asumimos que el estudiante estudiante posee este tipo de conocimientos y habilidades. Además de lo anterior, el programador orientado a objetos debe poseer un conocimiento firme sobre los conceptos específicos de este tipo de programación, los cuales son muy diferentes a los de otros paradigmas de programación. El objetivo de este libro es contribuir a construir el conocimiento de éstos conceptos de una manera simple y haciendo énfasis en el porqué fueron incorporados a este paradigma.
Otra habilidad necesaria para desarrollar programas orientados a objetos es precisamente la de crear “buenos” diseños. Es decir, programas mantenibles mantenibles (fáciles de entender y modificar), eficientes (que no desperdicien recursos) y robustos (que funcione adecuadamente en cualquier situación). Este tipo de habilidades sólo se puede desarrollar con la práctica pues cada problema presenta retos y alternativas diferentes y la experiencia es insustituible para lograr esta habilidad. Método de enseñanza.
Desde el primer capítulo se establece que la gran ventaja del paradigma orientado a objetos con respecto a otros paradigmas es que se modela el mundo real y esto se debe precisamente a que el mundo real está compuesto de objetos. Los conceptos que conforman la POO permiten este modelado y es precisamente en los conceptos en los que se hace énfasis en este libro. Cada concepto se presenta mostrando la situación del mundo real que se trata de modelar y después se traslada al dominio de la programación en el cual se formaliza a través de ejemplo de códigos y diagramas. En la gran mayoría de los conceptos se utilizan ilustraciones simples e informales realizadas a mano pero no por eso menos exactas.
Se repite la explicación de varias formas pues consideramos que muchas veces una sola forma de explicar no es suficiente. Se trata en todos los casos de presentar los conceptos a través de ejemplos sencillos y relacionados con la vida cotidiana. Se ha tratado de escoger ejemplos lo suficientemente simples para ilustrar los conceptos pero a la vez lo suficientemente completos para abarcar totalmente la idea que se pretende explicar. Los conceptos son presentados de manera informal para tratar de hacer más interesante la lectura. Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
7
Sin embargo, no se cae en la ambigüedad ni en la falta de seriedad rigurosa que el tema amerita. No es el objetivo de este libro explicar la teoría que subyace el desarrollo de software, más bien que el estudiante desarrolle la habilidad para analizar, diseñar y escribir código que resuelva problemas utilizando la programación orientada a objetos. Tampoco se incluye el diseño estructurado de algoritmos. Se asume que el lector conoce ya los conceptos básicos de la programación estructurada: tipos de datos, variables, operaciones aritméticas, asignación, estructuras de selección, estructuras iterativas, etc. El lenguaje de programación.
El lenguaje de programación utilizado en este libro es Java. Se escogió Java porque es un lenguaje muy poderoso, utilizado extensivamente en la industria y en las universidades. Java permite desarrollar prácticamente cualquier tipo de aplicación, desde una simple aplicación para un dispositivo móvil hasta una aplicación web con cientos o miles de clientes accediendo a ella de manera simultánea. Contenido del libro.
El capítulo I presenta una breve historia de los lenguajes de programación, así como conceptos fundamentales como las partes de una computadora, algoritmo, variables y constantes, tipos de datos, instrucciones de entrada/salida y estructuras de control. El capítulo II contiene los conceptos fundamentales de la programación orientada a objetos como son: abstracción, tipo de dato abstracto, clase, objeto, métodos, atributos. También presenta el software requerido para realizar los ejercicios propuestos así como los primeros ejemplos de código. El capítulo III contiene algunos conceptos adicionales de la programación orientada a objetos como son: métodos y atributos de clase, constructores, arreglos y métodos de acceso a los miembros de los objetos. El capítulo IV contiene conceptos avanzados de programación orientada a objetos como son: herencia, clases abstractas, interfaces, métodos abstractos, polimorfismo, covarianza, encajonamiento, clases internas, etc. El capítulo V contiene conceptos sobre el manejo de excepciones, su creación, manejo, excepciones que deben ser atrapadas, excepciones cuyo manejo es opcional, manejo de múltiples excepciones. También También se presenta en este capítulo el manejo de flujos avanzados de datos y cómo se utilizan estos flujos para almacenar información de los o bjetos de manera permanente. Cada capítulo presenta una breve introducción al tema principal y los conceptos básicos, posteriormente se presenta una serie de ejemplos explicados profusamente y termina con un resumen del capítulo. Finalmente, Finalmente, se proponen una una serie de preguntas y ejercicios ejercicios de autoevaluación.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
8
El autor espera que este libro sea de utilidad para los estudiantes que empiezan a construir su base de conocimientos en la programación de computadoras, de manera específica en la programación orientada a objetos. La premisa que ha guiado la escritura de este libro es que es mucho más efectivo adquirir pocos conocimientos de manera sólida al inicio que tratar de abarcar muchos conceptos de manera más ligera. La experiencia del autor como profesor es que la segunda opción provoca un total rechazo al material por parte de los estudiantes que recién han empezado sus estudios de programación. programación. Contacto con el autor: Cualquier sugerencia o comentario para mejorar este material, favor de enviar correo a:
[email protected]
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
9
1. Conceptos de Programación Computadora.
Empezaremos por definir el concepto fundamental en la programación que es precisamente el concepto computadora. Una computadora es una máquina capaz de procesar datos y mostrar resultados útiles (información) en muy diversas áreas del quehacer humano (Figura 1.1). 1 .1). Desde un videojuego hasta el cálculo de la trayectoria de un misil, o las estadísticas del censo de un país, o prácticamente cualquier actividad que requiera algún procesamiento de datos. Además de lo anterior, en los años recientes, se ha utilizado también como un dispositivo de comunicación tan versátil que ha generado cambios fundamentales en la forma en que los seres humanos se comunican. La computadora, junto con los avances en las comunicaciones digitales, ha acortado distancias, provocado movimientos sociales masivos, generado las fortunas más grandes de la historia y muchos otros cambios sociales y económicos. Muchos autores coinciden en que la época actual puede ser llamada la era de la información, en la cual las computadoras son parte fundamental.
Figura 1.1 Concepto Abstracto de Computadora Teniendo claro ya el concepto de computadora, el siguiente tema importante sería ¿Cómo funciona una computadora? Esta es una pregunta que se puede contestar de muchas formas. Una forma posible sería describir los circuitos, dispositivos y demás piezas físicas que la forman. Otra forma sería describir su funcionamiento desde el punto de vista teórico a través de definiciones matemáticas precisas, teoremas y demostraciones. Otra más, que es la que nosotros usaremos en este libro, es describir el funcionamiento de la computadora desde el punto de vista de un programador que utiliza el paradigma de orientación a objetos. Esta última sería una definición práctica. Es decir, lo que debemos saber para poder programarla y así resolver los problemas que se presenten.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
10
Podemos ver una computadora como un dispositivo que contiene los siguientes componentes (ver figura 1.2):
Una memoria, o almacén de datos, en donde almacenamos información sobre los objetos que estamos representando y en la cual podemos añadir, grabar, eliminar y modificar información. Por ejemplo, si estamos representando r epresentando un automóvil, podemos almacenar su marca, modelo, número de motor, fotografía, número de placas, etc. Esta memoria recibe el nombre de “ memoria heap” porque heap es una palabra inglesa que significa “montón” en español y se refiere a la forma en la cual se almacenan los objetos. Los cuales se “amontonan” sin seguir una secuencia estructurada. Otra memoria que contiene la computadora es una memoria temporal que sólo se utiliza al realizar operaciones y que se limpia al terminar de realizar dichas operaciones. Esta memoria recibe el nombre de “ stack ” o “ pila” porque los datos almacenados en esta memoria son accedidos en orden inverso al cual fueron introducidos. El funcionamiento de esta memoria simula el de una pila de platos en un restaurante. Los platos que están accesibles en la parte superior de la pila son los últimos platos que se introdujeron a la misma. Por ejemplo, si en un programa queremos calcular la antigüedad de un automóvil, debemos de restar al año actual el modelo. En java escribiríamos la expresión antiguedad = añoActual – modelo; Suponiendo que año actual es 2012 y el modelo es 2009. Entonces, en la memoria de pila almacenaríamos el 2012, 2009 y 3 que es el resultado de la resta. No nos preocupemos ahora por el orden en el que dichos datos son introducidos. El valor del modelo del del automóvil estará duplicado duplicado por un momento momento porque estaría en la pila y en el heap. Sin embargo, al terminar de realizarse la operación, los valores de la pila son borrados. Además de las dos memorias descritas anteriormente, las computadoras tienen un “cerebro” llamado CPU (Central CPU (Central Processor Unit) que es el que interpreta las instrucciones y las ejecuta. Generalmente estas instrucciones cambiarán los datos almacenados en las dos primeras memorias mencionadas. Finalmente, tenemos los dispositivos llamados periféricos que son impresoras, discos duros, la pantalla, teclado (también llamado consola por razones históricas), ratón, etc. Estos dispositivos nos permiten “comunicarnos” con la computadora. Por ejemplo, por medio del teclado podemos introducir el modelo del automóvil, por medio de la pantalla podemos conocer la la antigüedad del mismo. mismo. El disco duro se le le llama también memoria memoria secundaria porque también almacena información pero esta no es directamente accesible para procesarla, es necesaria “cargarla” primero en cualquiera de las dos memorias mencionadas anteriormente.
La principal razón por la cual son necesarias dos diferentes memorias es que la computadora necesita almacenar la información en diferentes formas. Por ejemplo, los objetos tienen muy diversos tamaños y sería imposible almacenarlos en una memoria que tuviera una estructura muy rígida. Por esta razón los objetos se almacenan en la memoria heap. Por otro lado, el stack es necesario porque es muy común que la información deba ser retribuida en orden inverso al que Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
11
fue introducida. Pensemos en que alguien está haciendo una tarea y recibe una llamada telefónica, decide atender la llamada y en esos momentos suena la alarma de la estufa (Suponemos una estufa muy moderna) avisando que la comida está por quemarse. En el stack (memoria) de la persona se introducen los eventos en el orden: tarea, teléfono, estufa. Obviamente, el orden en el cual esta persona atiende a cada uno de ellos es inverso al orden en el cual sucedieron: estufa, teléfono, tarea. Es decir, primero apaga la estufa, después sigue atendiendo la llamada (esperamos que la otra persona no haya colgado) y finalmente continua haciendo su tarea. De manera similar, las computadoras deben interrumpir algún proceso o función para efectuar otro y deben guardar toda la información del proceso para luego continuar con el primero. Sería algo similar a la famosa frase que usamos los humanos ¿En qué estaba? Una vez que se ha completado la tarea, se elimina la información de la misma y se continúa con la tarea que se estaba realizando. Los programas se almacenan en otra memoria llamada “memoria “memoria del programa”, programa”, la cual es necesaria porque ahí se almacenan las instrucciones que se van a realizar. El motivo por el cual esta memoria no fue incluida en la descripción abstracta de computadora se debe a que no haremos referencia a ella en este libro. Esta es pues la computadora como la veremos en este libro. En realidad, las computadoras son más complejas y existen aún más componentes. Sin embargo, por ahora con esto nos basta para escribir nuestros primeros primeros programas. En este libro, trabajaremos trabajaremos directamente con la memoria heap, el stack, la pantalla y el teclado. Al final del libro, en el capítulo 7, usaremos la memoria secundaria para almacenar información. El uso de la impresora, ratón y otros dispositivos periféricos queda fuera del alcance de este libro.
Figura 1.2 Partes de una Computadora para Programación Orientada a Objetos Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
12
Hemos visto de manera muy abstracta las partes y el funcionamiento de la computadora. Estudiaremos a continuación los lenguajes de programación, sus orígenes y sus características. Lenguajes de Programación.
Un lenguaje de programación nos permite comunicarle a la computadora “qué debe hacer” para resolver un problema. Constan de una serie de reglas gramaticales y símbolos que las computadoras interpretan de manera exacta y sin ambigüedades. Los lenguajes de programación nos permiten escribir programas. Los programas son las series de símbolos escritos siguiendo las reglas gramaticales de los lenguajes de programación que instruyen a la computadora lo que debe hacer para resolver un problema. Aunque algunos autores mencionan que los programas son series de instrucciones, eso no es enteramente cierto para todos los lenguajes de programación. Por ejemplo, los los programas escritos en el lenguaje PROLOG PROLOG no consisten consisten de instrucciones instrucciones (órdenes) sino de definiciones lógicas. Paradigmas de Lenguajes de Programación.
Existen varios tipos, estilos o paradigmas de los lenguajes de programación: Programación secuencial o imperativa, concurrente, funcional, orientada a objetos, simbólica, orientada a eventos, entre otros. Estos paradigmas o formas de programar producen lenguajes muy diferentes no sólo en su sintaxis sino en la forma como el programador “ve” a la computadora. Por ejemplo, el paradigma orientado a objetos permite al programador concebir a la computadora como fue descrita anteriormente. Sin embargo, utilizando la programación lógica el programador concibe a la computadora como un razonador lógico capaz de concluir o inferir resultados basados en las definiciones dadas por él. El estudio de los diferentes lenguajes y paradigmas de programación es muy fascinante pero queda fuera de los alcances de este libro. Para que el lector pueda contrastar tres diferentes paradigmas de programación, a continuación presentamos las ideas principales de la Programación imperativa, de la programación Lógica y de la programación progr amación orientada a objetos Programación imperativa.
Este paradigma fue el primero que se utilizó porque está estrechamente ligado al funcionamiento real de las computadoras. En este paradigma, se describe el estado de la computadora y el cambio de estado que va sufriendo la máquina a través de la ejecución ejecución de instrucciones. La computadora sólo tiene un área de de memoria en donde se almacenan datos y programas. A este modelo se le le llama “Máquina de Von Neumann Neumann (Ver figura 1.3) Los tipos de datos que pueden almacenarse son simples: enteros, flotantes, caracteres, etc. No existe la posibilidad de almacenar objetos con sus características (atributos y métodos). Lenguajes como C, Pascal, Basic pertenecen a este paradigma.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
13
Figura 1.3 Modelo (muy) Abstracto de la Máquina de Von Neumann Entre los conceptos principales que tiene este tipo de programación se encuentran los siguientes:
Tipos de datos. Un tipo de dato define el conjunto de valores posibles que se pueden
almacenar en un área de la memoria. Por ejemplo, enteros, flotantes, caracteres, etc. Variables. Las variables son áreas de la memoria representadas por un identificador que pueden almacenar un dato. Por ejemplo, la variable edad podría ser usada en un programa para almacenar la edad de una persona. Normalmente sería una variable de tipo entero. Lo que significa que sólo puede almacenar números enteros (dentro de un rango permitido por los límites físicos de la memoria). m emoria). Estructuras de control . Una variante de la programación imperativa es la programación estructurada, en la cual los programas se escriben utilizando una serie de estructuras de control que representan el orden en el que se ejecutarán las instrucciones. Existen estructuras de selección como el if-then, el if-then-else y estructuras iterativas como el while, do-while y el for . Instrucciones de Entrada/Salida . Para que el programa pueda interactuar con el usuario, son necesarias las instrucciones de entrada/salida, las cuales permiten introducir información a la computadora y mostrar información en algún dispositivo como la pantalla o la impresora. Constantes. Existen valores que son conocidos y que no cambian bajo ninguna circunstancia. Por ejemplo, la constante (“Pi”) cuyo valor aproximado es 3.1415926535897932384626433. Este valor puede ser utilizado directamente en un programa para realizar cálculos. Funciones. El concepto de función en programación es muy similar al concepto de función matemática. Es un mapeo entre uno o varios valores de entrada llamados argumentos de la función y un valor de salida el cual decimos que es “el “ el valor que regresa la función”. Así
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
14
si la función f(x)=x+2; entonces f(2)=4 y f(5)=7. En este tipo de lenguajes, el programador puede definir funciones y ejecutarlas cuando sea necesario. A continuación presentamos un programa escrito en lenguaje C (Figura 1.4) que lee un número y calcula e imprime su factorial. Recordemos que el factorial de un número es la multiplicación de todos los números enteros entre 1 y el número. Así, el factorial de 4 es: 1x2x3x4 = 24. La operación del factorial se representa por el símbolo de admiración. Entonces 4! = 24.
Figura 1.4 Programa en C que calcula y muestra el factorial de un número. Las líneas 1 y 2 incluyen funciones estándar que son necesarias para la entrada y salida de datos las cuales se realizan en las líneas 8,9 y 14. Las líneas 5,6 y 7 declaran variables enteras. Es decir, variables que sólo podrán contener números enteros. La línea 5 no asigna un valor inicial a la variable numero a diferencia de las líneas 6 y 7 las cuales si asignan valores iniciales a las variables i y factorial respectivamente. Esto es debido a que el valor que tomará la variable numero será dado por el usuario del programa. Para esto se requiere que el programa “lea” el valor valor del teclado de la computadora. La instrucción que realiza esa lectura está en la línea 9. La línea 10 corresponde a la estructura iterativa while. Esta estructura indica a la computadora que mientras el valor de la variable i sea menor o igual al valor de la variable numero se ejecutarán las instrucciones de las líneas 11 y 12. La línea 14 muestra en la pantalla el valor del número y de su factorial. La línea 15 hace una pausa en la ejecución ejecución del programa para que el usuario usuario pueda ver el resultado. La ejecución del programa se muestra en la figura 1.5.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
15
Figura 1.5 Resultado de la ejecución del programa en C del cálculo del factorial de un número Programación Lógica
Este tipo de programación utiliza definiciones matemáticas a través de predicados lógicos. Este paradigma de programación se le llama declarativa debido a que el programador no especifica de manera procedural (o (o sea, instrucción por instrucción) el programa, sino que escribe declaraciones a través de predicados que le permiten “razonar” a la computadora y obtener un resultado. Utilizando la definición matemática del factorial:
es realmente muy simple escribir un programa en el lenguaje PROLOG que calcule el factorial de un número. En la figura 1.6 de muestra un listado del programa.
Figura 1.6 Programa en PROLOG para el cálculo del factorial de un número Como puede observarse fácilmente, el programa en PROLOG está basado en la definición matemática del factorial. El programador no tiene que preocuparse por escribir estructuras iterativas, ni pensar en el estado de la computadora ni como las instrucciones afectan dicho estado. El programa de la figura 1.6 es básicamente la misma m isma definición matemática que se mostró anteriormente. La línea 1 representa la línea superior de la definición. La líneas 2 a 5 representan la línea inferior de la definición. definición. El símbolo símbolo ”:-“representa el “ si ” de la definición. No consideramos que sea necesario explicar a detalle todo el código. Tampoco esperamos que el lector comprenda a detalle el código, es suficiente con comprender de manera general que en este tipo de programas, el programador debe especificar la definición de un problema sin preocuparse por cómo resolverlo. La salida y ejecución del programa se muestra en la figura 1.7 Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
16
Figura 1.7 Resultado de la ejecución del programa en PROLOG del cálculo del factorial de un número Programación Orientada a Objetos
En el siguiente capítulo hablaremos extensamente de la programación orientada a objetos. Sin embargo, como una breve introducción a este paradigma, presentaremos a continuación el mismo ejemplo del factorial resuelto en el lenguaje Java, el cual es uno de los lenguajes más representativos del paradigma orientado a objetos. Como ya se mencionó anteriormente en este capítulo, el programador que utiliza el paradigma orientado a objetos visualiza a la computadora como una máquina capaz de representar internamente objetos con sus propiedades (atributos y métodos). Los programas orientados a objetos constan de un grupo de objetos que se comunican a través del paso de mensajes. La figura 1.8 muestra el código en el lenguaje Java que calcula e imprime el factorial de un número. No explicaremos a detalle el código pues lo haremos a lo largo del libro. Sólo pedimos al lector observar las líneas 21 y 22. La línea 21 crea un objeto de la clase Calculador. Este objeto sabe cómo calcular un factorial. ¿Cómo lo sabe? Porqué esto fue establecido en las líneas 5-12 del código. Finalmente, la línea 22 envía un mensaje al objeto c solicitándole que calcule el factorial del número leído. La figura 1.9 muestra el resultado de la ejecución del programa. Lo primero que salta a la vista del listado del programa en Java es que es más complicado que los otros dos ejemplos presentados. Entonces… ¿Para qué complicarse la vida? ¿Para qué usar Java si usando C (o PROLOG) podríamos resolver el problema de manera más sencilla? La respuesta es que para muchos problemas más complejos (que son los que probablemente tengamos que resolver en la vida profesional) es más adecuado este paradigma. Por ahora, el lector tendrá que confiar en la palabra del autor. Espero que al final de la lectura del libro quede convencido de las ventajas del paradigma orientado a objetos. El objetivo de presentar este pequeño ejemplo del cálculo del factorial de un número utilizando tres diferentes paradigmas de programación es mostrar al lector tres formas completamente diferentes de resolver un problema y como el programador que utiliza PROLOG debe pensar de manera muy diferente al programador que utiliza C o Java. Mientras que la programación Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
17
imperativa obliga al programador a pensar en una máquina que “obedece” instrucciones y que va cambiando su estado de acuerdo a la ejecución de dichas instrucciones, instrucciones, en la programación lógica, el programador debe pensar en cómo escribir la definición matemática y dejar que la máquina utilice esa definición para encontrar la solución al problema. Por otro lado, el programador que utiliza la orientación a objetos resuelve el problema en términos de los objetos relevantes al mismo problema, sus atributos, sus métodos y escribe un programa teniendo en mente la interacción de los mismos.
Figura 1.8 Programa en Java para el cálculo del factorial de un número Podríamos presentar al lector las diversas soluciones del mismo problema utilizando otros paradigmas de programación pero consideramos que con eso es suficiente para que se tenga una idea general de la existencia de diferentes formas de programar y que cada paradigma obliga al programador a pensar en términos de la máquina que ejecutará el programa.
Figura 1.9 Resultado de la ejecución del programa en Java del cálculo del factorial de un número
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
18
Breve Historia de los Lenguajes de Programación.
La historia de los lenguajes de programación es muy larga. Algunos consideran que la historia inicia desde la invención del telar automático en 1801. Para generar los patrones en las telas, se utilizaron tarjetas perforadas que contenían un código que se puede considerar como un antecesor de los lenguajes de programación. Sin embargo, los primeros lenguajes que permitían escribir un programa para que fuera ejecutado por una computadora aparecieron a principios de los años cuarentas. Estos lenguajes estaban completamente ligados a la computadora para la cual se escribía el programa. El programador tenía que escribir el programa en términos de registros, operaciones aritméticas simples, tipos de datos básicos, etc. Por esta razón, a este tipo de lenguajes se les llama lenguajes de bajo nivel. No fue sino hasta 1955 que se creó el primer lenguaje de alto nivel (o sea que el programador puede escribir su programa en términos del problema y no de la computadora). Este lenguaje llamado FORTRAN como acrónimo de “FORmula TRANslator”. Posteriormente se crearon otros lenguajes de más alto nivel. Como LISP en 1958, COBOL en 1959. El paradigma de la programación programación estructurada nació nació con el lenguaje Pascal creado por Niklaus Wirth publicado publicado en 1970. El lenguaje C fue creado entre 1969 y 1973. A mediados de los setentas se creó Smalltalk el cual sentó las bases para el desarrollo de los lenguajes orientados a objetos. El paradigma de la programación lógica nació con el lenguaje PROLOG en 1972. Aunque se puede decir que la programación orientada a objetos nació en los setentas con Smalltalk, no fue sino hasta los noventas cuando fue adoptada masivamente por la industria y la academia. En la actualidad actualidad existen registrados registrados más de ¡2500 ¡2500 lenguajes de de programación! El lector interesado puede conseguir un poster publicado por O’reilly en http://oreilly.com/news/languageposter_0504.html el cual contiene una gráfica de tiempo razonablemente completa de los lenguajes de programación y sus relaciones. Por cierto, ¡Este poster tiene que ser impreso en varias hojas o en un plotter debido a su tamaño! Resumen del capítulo.
En este capítulo describimos de manera muy abstracta el concepto de computadora, sus partes y su funcionamiento también de manera muy abstracta. Vimos que dependiendo del paradigma de programación utilizado, el programador conceptualiza a la computadora de manera diferente, en la programación procedural o imperativa, el programador considera que la máquina tiene cierto estado (dado por los valores almacenados en su memoria) y debe escribir las instrucciones que cambien el estado para resolver el problema. En el paradigma orientado a objetos, el programador considera que la computadora es capaz de representar los objetos con sus métodos y atributos en su memoria y escribe el programa de tal forma que los objetos reaccionen a través de envío de mensajes. En la programación lógica, el programador visualiza a la computadora como un “razonador” el cual es capaz de resolver resolver un problema dada una definición (que el programador provee) a través de predicados lógicos. Ejercicios y preguntas
1. Mencione la diferencia principal entre el modelo de computadora de Von Newmann presentado en la figura 1.3 y el modelo de computadora de computadora de la figura 1.2 2. ¿Cuál es la diferencia entre dato e información? Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
19
3. Mencione un ejemplo de la vida cotidiana que sea análogo al funcionamiento de un stack, es decir que el último evento que sucede, es el primero en ser procesado. 4. ¿Cuál es la relación entre paradigma de programación, lenguaje de programación y programa? 5. ¿De los lenguajes de de programación mencionados en este capítulo, capítulo, cuál sería el más conveniente para calcular el área de un círculo dado su radio? Justifique su respuesta 6. ¿Cuál es la diferencia entre un lenguaje de alto nivel y un lenguaje de bajo nivel? 7. ¿Por qué en la abstracción de computadora para la programación orientada a objetos son necesarias dos tipos de memorias? 8. Investigue sobre el paradigma de la programación funcional y compare este paradigma con respecto a los otros paradigmas vistos en este capítulo. 9. Considere las siguientes tareas: Anudar los lazos de los zapatos, realizar una división aritmética, manejar una bicicleta y tocar la guitarra. ¿Cuáles se podrían se descritos más fácilmente de manera procedural ? 11. Complete el siguiente mapa conceptual etiquetando los arcos con las letras correspondientes
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
20
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
21
2. Introducción a la Programación Orientada a Objetos En este capítulo veremos los conceptos fundamentales de la programación orientada a objetos y empezaremos a escribir los primeros programas utilizando Java. El procedimiento para la instalación de Java, compilación y ejecución de programas no se describe aquí. Toda la información y software necesario se puede obtener del sitio web de Oracle: www.oracle.com. Programación Orientada a Objetos.
La programación de computadoras consiste en solucionar un problema a través de un código escrito en algún lenguaje de programación. programación. La Programación Orientada a Objetos (POO) consiste en identificar objetos objetos que sean relevantes relevantes para la solución de un problema y llevar a cabo una una representación de dichos objetos y de sus relaciones en la computadora a través de un programa. Se trata de modelar el mundo real, o sea el mundo en donde está el problema que se quiere resolver, en la computadora (ver figura 2.1)
Figura 2.1 La programación orientada a objetos trata de modelar el mundo real El mundo en el que vivimos está lleno de objetos. Hay objetos en todas partes. Algunos son tangibles, otros intangibles pero de cualquier forma los podemos identificar y describir sus características. Por esta razón, la POO es el paradigma de programación que de manera más natural permite que el mundo real sea modelado a través de la representación de los objetos Los objetos tienen atributos y métodos (ver figura 2.2). Los atributos son las características que tienen los objetos y los métodos son las funciones que el objeto puede realizar. Por ejemplo, consideremos la representación del objeto teléfono dentro de la computadora. Los atributos del Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
22
teléfono podrían ser su tamaño, color, nivel de carga de la batería, tamaño de la pantalla, etc. Los métodos o funciones del teléfono podrían ser llamar, colgar, recibir llamada, etc. Es importante señalar el párrafo anterior se utiliza “ podrían” varias veces. Esto es porque porque los objetos, atributos y métodos que se determinen dependerán completamente del problema a resolver. Para un problema determinado podría ser relevante el color del teléfono mientras que para otro problema ese atributo sería completamente irrelevante. Por ejemplo, si queremos escribir un programa que permita registrar y controlar la información académica de los estudiantes de una universidad, los atributos relevantes serían el nombre, matricula, promedio, carrera, etc. Pero si el objetivo objetivo del programa es encontrar afinidades para establecer establecer relaciones relaciones de amistad o románticas entonces los atributos relevantes serían otro tipo de datos personales como religión, pasatiempos, tipo de música que le gusta, etc.
Figura 2.2 Los objetos o bjetos tienen atributos y métodos Programación Orientada a Objetos vs Programación Imperativa.
En la actualidad existe cierta controversia en cuanto a la relación entre la programación orientada a objetos y la programación imperativa. Algunas personas argumentan que en realidad, no hay una gran diferencia excepto por el hecho de que en la programación orientada a objetos se pueden definir tipos abstractos de datos (también llamadas “clases”). “clases”) . Otro punto de controversia es sobre qué debe enseñarse primero. Algunos profesores de estas materias creen que es mejor enseñar primero la lógica de programación sin considerar objetos, mientras que otros argumentan que es más conveniente enseñar con objetos desde el principio. Algo que vale la pena comentar es que en la programación imperativa, el programador normalmente está limitado a cierto tipo de datos, como enteros, reales, etc. Algunos lenguajes Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
23
imperativos permiten crear tipos de datos, pero aún así están muy limitados con respecto a lo que se puede hacer con los lenguajes orientados a objetos. En la programación orientada a objetos, el programador declara sus propios tipos de datos (a los que se les llama tipos de datos abstractos). Por ejemplo, personas, árboles, estudiantes, etc. Estos tipos de datos abstractos no sólo contienen datos sino también métodos. Abstracción.
Lo primero que debe realizar un programador para escribir un programa utilizando el paradigma orientado a objetos es un proceso de abstracción. La abstracción consiste en eliminar lo que no es importante o relevante para un fin determinado y conservar lo relevante o importante. Como se muestra en la figura 2.3, cada quien extrae de la escena lo que es relevante para él o ella. De manera análoga, el programador debe entender muy bien el problema a resolver e identificar mediante un proceso de abstracción abstracción cuales serían los objetos objetos relevantes que podrían utilizarse para resolver un determinado problema. Una vez identificados los objetos, el programador debe establecer cuáles son las características (atributos) de los objetos que son relevantes al problema para representarlos en la computadora. Finalmente, también debe establecer cuáles son las funciones (o métodos) relevantes que cada objeto debe realizar para la solución al problema.
Figura 2.3 El proceso de Abstracción Ejemplo: Cálculo de la Nómina de una Empresa.
Veamos un primer ejemplo: Se desea calcular el pago a un grupo de trabajadores de una empresa. A este tipo de programas se les llama “Sistema de Nómina”. La gran mayoría de las empresas tienen un sistema de nómina que les permite calcular el pago a los trabajadores. Tomaremos este Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
24
ejemplo para explicar varios conceptos importantes de la programación orientada a objetos y escribiremos código que implementa dichos conceptos. La empresa desea calcular el pago de acuerdo a las horas trabajadas y el salario por hora. El reporte de la nómina deberá ser parecido a lo que aparece en la figura 2.4
Figura 2.4 Reporte de la nómina de la empresa Acme Empezaremos por identificar cuáles serían los objetos del mundo real que sería conveniente representar en la computadora. Obviamente, los objetos más relevantes para este problema son los trabajadores. Sin Sin embargo, podemos encontrar encontrar otros objetos como la propia empresa, empresa, el sueldo, el pago, etc. La decisión de que objetos se representarán no es única para cada problema. Depende de la experiencia, habilidad y sentido común del programador. Por ejemplo, el salario de los trabajadores lo podríamos considerar como un objeto o como un atributo del objeto trabajador. En este ejemplo, tomaremos la segunda opción. Consideraremos al salario por hora, a las horas trabajadas y al nombre como atributos de cada trabajador. En cuanto a los métodos, consideraremos sólo un método que sería el cálculo del pago. Probablemente, el lector pueda pensar que el cálculo del pago no debería hacerlo el trabajador. De hecho, en la vida real, sería muy raro que cada trabajador calcule su pago. Pero por conveniencia lo implementaremos de esta forma. Para este ejemplo, empezaremos considerando únicamente al objeto trabajador con sus atributos: nombre, salario salario por hora y horas trabajadas. El único método que consideraremos inicialmente es calcular el pago. Más adelante, mejoraremos el diseño agregando otros objetos. Puede el lector contestar la siguiente pregunta: ¿Por qué no elegimos el pago como atributo del objeto trabajador? La respuesta la encontraremos más adelante. Clases y Objetos
Como se mencionó anteriormente, un programa orientado a objetos es un grupo de objetos que interactúan enviándose mensajes. Como sucede en la vida real, (recuerde el lector que la programación orientada a objetos trata de modelar el mundo real) para crear un objeto es necesario construir dicho dicho objeto. Para construir los objetos, la programación orientada a objetos utiliza moldes que reciben el nombre de clases. Así, para construir un objeto en el programa necesitamos primero definir definir su clase. Imagine que los objetos objetos son pasteles y las clases clases son los moldes en los que se cocinan los pasteles (ver figura 2.5).
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
25
Figura 2.5 Relación entre Clase y Objeto Existen varias analogías que podemos establecer de la figura 2 .5. Veamos la siguiente lista: var ios pasteles, análogamente, con una clase podemos crear Con un molde podemos crear varios varios objetos. Todos los pasteles que se crearon con el mismo molde tienen la misma forma, análogamente, todos los objetos de la misma clase tendrán las mismas características (atributos y métodos). Aunque dos (o más) pasteles se creen con el mismo molde, cada pastel ocupa un lugar en el espacio y puede tener diferentes valores en sus atributos. Por ejemplo, cada pastel podría tener diferente sabor (vainilla ó chocolate). Análogamente, cada objeto que se cree de la misma clase ocupa su propio espacio en la memoria heap y puede tener diferentes valores en sus atributos. Los moldes también pueden tener sus propios atributos (por ejemplo, el material del que están hechos, sus dimensiones, etc.). Análogamente, las clases también pueden tener sus propios atributos (y métodos). Creación de una clase en Java.
Para crear una clase en Java, se tiene que escribir en un archivo de texto con la extensión “java” y definir sus métodos y sus atributos. La sintaxis (mínima) es la siguiente: class
{
} Para el ejemplo de la nómina, escribimos el código de la figura 2.6 para definir la clase (molde) del cual podremos construir trabajadores más adelante en el programa.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
26
Figura 2.6 Definición en Java de la clase Trabajador Es necesario aclarar varios detalles de la figura 2.6. Primero que nada, es un archivo de texto que puede crearse con cualquier cualquier editor de textos. Puede tener cualquier cualquier nombre, pero debe tener la extensión “java”. Es “java”. Es una buena costumbre poner el nombre del archivo igual al nombre de la clase. Por lo que nombraremos a este archivo “Trabajador.java”. Los números de líneas NO se escriben en el archivo, fueron incluidos para poder hacer referencia a cada línea del código fácilmente. Empecemos con la línea 1, en ella declaramos el nombre de la clase Trabajador. Por convención internacional, el nombre de la clase debe empezar con letra mayúscula. Las líneas 2,3 y 4 declaran los atributos con sus tipos de datos de los objetos de la clase Trabajador. Note que el nombre es de tipo String lo que significa que podremos almacenar cualquier caracter en este atributo. Los atributos salarioPorHora y horasTrabajadas son de tipo entero (int) lo que significa que sólo podremos almacenar números enteros en ellas. También por convención internacional, los nombres de los atributos empiezan con minúscula y si tienen varias palabras, cada palabra empieza con mayúscula (excepto la primera, la cual comienza con minúscula). A esta notación se le llama notación “camello” por las jorobas que se forman al inicio de las palabras (ver figura 2.7).
Figura 2.7 Notación Camello
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
27
La razón por la que no incluimos el atributo correspondiente al pago semanal es porque puede ser calculado fácilmente multiplicando multiplicando el salario por hora por las horas trabajadas. t rabajadas. Los nombres de las clases de los atributos y los métodos no pueden llevar espacios. Si se ponen ponen espacios espacios entre ellos, ellos, el programa no compilará. La línea 5 está en blanco. Las líneas en blanco se ignoran. La línea 6 define el método llamado calculaPago(). Es sumamente importante aclarar varias cosas sobre la definición de los métodos. Primero, debemos tener muy claro que los métodos, como los atributos, también tienen asociado un tipo de dato. dato. En este caso, el tipo de datos datos es int, lo que significa que cada vez que un objeto de la clase Trabajador calcule su pago, la cantidad resultante será un entero (o sea, no podrá contener cifras decimales). Después del nombre del método siempre se escriben paréntesis. En este caso no se escribe nada adentro de los paréntesis porque este método no recibe parámetros (más sobre esto después). Algo muy importante que el lector debe notar es que los métodos que regresan valores siempre deben tener una instrucción return (línea 7). La instrucción return le indica al programa que la ejecución del método termina y que la expresión que está a la derecha de la palabra return debe ser evaluada (calculada) y el valor resultante es el resultado del llamado del método. En este caso, el objeto multiplica el valor de sus atributos salarioPorHora y horasTrabajadas. Haciendo una analogía con el mundo real, sería como si instruyéramos al objeto de la clase trabajador lo siguiente: “Cuando te pidan calcular tu pago, multiplica tu sueldo por las horas que trabajaste y el resultado resultado de la multiplicación es tu respuesta a la solicitud” La línea 8 contiene solamente la llave que cierra, la cual corresponde a la llave que fue abierta en la línea 6. Los métodos se delimitan con llaves y las instrucciones que estén adentro de las llaves se ejecutarán cada vez que se ejecute ese método. La línea 9 cierra la llave abierta en la línea 1. Creación de objetos.
Hasta ahora, ya creamos la clase (o molde) con la cual podremos crear objetos de la clase trabajador. Todos los trabajadores que se creen van a tener como atributos nombre, salario por hora y horas trabajadas. También van a “saber” calcular su pago ( salario * horasTrabajadas). Ahora completaremos el programa creando una clase llamada Main, la cual contendrá un método también llamado main el cual creará los tres objetos, asignará valores a los atributos y les les enviará sendos mensajes (sendos (sendos = a cada uno). uno). La figura 2.8 muestra el código de la clase Main completo. Antes de empezar con el desglose línea por línea de la clase Main, necesitamos entender porqué creamos la clase Main si no existe ningún objeto de esa clase en el mundo real. La razón es la siguiente: Los programas orientados a objetos se basan en la creación de objetos y en el paso de mensajes entre ellos. Sin embargo, para que puedan existir esos objetos, “alguien” los los tiene que crear y empezar el proceso de envío de mensajes. Ese precisamente es el objetivo de la clase Main. Ahora... ¿Por qué se llama “Main” Main”?... Por razones históricas. La sintaxis de Java está basada en gran medida en el lenguaje C++, el cual llama a su método inicial “main” (que por cierto
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
28
significa principal en en español). Aunque podemos utilizar cualquier otro nombre para esta clase, se acostumbra ponerle “Main” Main” a la clase porque el método de esa clase que se ejecuta al principio se llama “main” y ¡ese ¡es e nombre si es obligatorio! Empecemos ahora a desglosar desglosar el código de la figura 2.8. La línea 1 define el nombre de la clase, la la línea 2 define el método main. Note por favor que el método main debe tener las palabras: public, static y void. La razón de las dos primeras la veremos más adelante, la razón de la tercera (void) es porque este método no regresa ningún valor. Los métodos que no regresan ningún valor no requieren del uso de la palabra “ return” como vimos anteriormente. Todo lo que está adentro de los paréntesis paréntesis “(String[] args)” indica el tipo de parámetros que este método recibirá. Por lo pronto, tendremos que aprendernos de memoria la línea 2 y la usaremos tal cual en todos los programas de este libro. Más adelante adelante veremos la razón para cada una de sus partes.
Figura 2.8 Código de la clase Main para el cálculo de la Nómina
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
29
La línea 3 es muy importante y debemos entenderla por completo. Lo primero que se hace es declarar la variable trab1 de tipo Trabajador. Esto significa que la variable trab1 sólo podrá almacenar objetos de la clase Trabajador (no enteros, no reales, no cadenas, sólo trabajadores). A la derecha del símbolo “=” tiene la instrucción: new Trabajador(). Esta instrucción provoca que se cree un trabajador en la memoria heap (ver figura 1.2). A partir de la línea 3, podemos decir que ya hemos creado un objeto y que este objeto reside en la memoria heap. Sin embargo, este objeto creado no tiene ni nombre, ni salario por hora, ni horas trabajadas. Precisamente de esa asignación de valores se encargan las líneas 4,5 y 6. Observe que para hacer referencia a un atributo de un objeto, se utiliza un punto. De esta forma trab1.nombre hace referencia al atributo nombre del objeto que está almacenado en la variable trab1.Las líneas 7 a la 14 hacen algo similar pero con los otros dos objetos que deseamos crear. De esta forma, el estado de la computadora después de haber ejecutado la línea 14 se representa en la figura 2.8. 2.8 .
Figura 2.9 Estado de la computadora después de haber creado los tres objetos o bjetos y asignado los valores de sus atributos. En la figura 2.9, podemos observar que después de haber ejecutado la línea 14, ya existen tres objetos creados en la memoria heap. Existen tres variables locales locales en el stack (trab1, trab2 y trab3) y cada de una de esas variables locales “apunta” a cada uno de los objetos creados. Por el momento, no es necesario entender a fondo como funciona a detalle el concepto de apuntador. Más adelante en el libro veremos con mucho mayor grado de detalle cómo una variable “apunta” a un objeto creado en la memoria heap. Por ahora, lo único que debe quedar claro es que
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
30
podemos referenciar (o sea, asignar o leer valores) de los atributos de los objetos a través del nombre de la variable, seguida de un punto, seguido del nombre del atributo. Las líneas 19 a 21 provocan que se muestre en la pantalla toda la cadena que escriba entre comillas. Los espacios, los tabuladores (\t), etc. La línea 19 es una continuación de la línea 18. En Java, las cadenas de caracteres (o Strings) se pueden concatenar con el operados “+”. Los Strings siempre se escriben entre comillas. comillas. Entonces, lo que hace la línea 19 es concatenar concatenar el String que se empezó en la 18 con el String de la línea 19. Además, en Java podemos separar las líneas de programación sin ningún problema. Esto le puede agregar mayor claridad y elegancia al programa. La línea 22 es sumamente importante y es necesario que la desglosemos minuciosamente. Es fundamental que no se continúe con la lectura si no se ha entendido completamente lo que pasa trab1.calculaPago() o()primeramente al ejecutarse esta línea. Al ejecutarse int pago1 = trab1.calculaPag se declara que la variable pago1 es entera, como hemos mencionado anteriormente, esto significa que sólo podrá contener valores enteros. A la derecha del símbolo igual, se está enviando un mensaje al objeto contenido en la variable trab1. Este mensaje le solicita al objeto que ejecute su método calculaPago(). Note que es muy importante que el tipo de dato de la variable pago1 coincida con el tipo de dato del método calculaPago() pues el resultado del cálculo del pago será asignado a la variable pago1. Imagínese que usted le pregunta a un obrero: ¿Cuánto te debo pagar esta semana? ¡Obviamente, usted espera un número!, no sería lógico que el obrero le contestara: “Departamento de Producción”. Por eso es muy importante que los métodos definan el tipo de valor que regresarán. En el caso de los trabajadores, esto se efectúa en la línea 6 de la figura 2.6. Cuando se ejecuta la instrucción: trab1.calculaPago(), se pasa el control del programa al objeto trab1, el cual, realiza la multiplicación de los valores de su salario por hora y sus horas trabajadas y regresa el valor calculado para que sea asignado a la variable pago1. Eso ya lo debemos tener bien claro, pero… ¿De dónde obtiene los valores de sus atributos (horas trabajadas y salario diario)? ¿Podría el lector decir en qué líneas líneas se asignan estos valores? Exactamente, en las líneas 5 y 6 de la figura 2.6. De esta forma, cuando a un objeto se le asignan valores a sus atributos, éstos se conservarán por todo el programa, a menos que nosotros destruyamos el objeto (más sobre ese tema después) de manera explícita o implícita. La líneas 23 y 24 muestran en la pantalla, los datos para el pago del primer trabajador. Las líneas restantes muestran los datos del pago de los otros trabajadores. El resultado de la ejecución del programa se muestra en la figura 2.10
Figura 2.10 Resultado de la ejecución del programa de la figura 2.8 Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
31
En este momento es muy m uy importante que el lector haya compilado y ejecutado el ejemplo. En caso de que no lo haya hecho, le sugerimos enormemente que lo haga pues la única forma de aprender a programar es escribiendo la mayor cantidad de programas. Existe una cantidad tan grande de detalles que suceden cuando se escribe un programa, que resulta prácticamente imposible enumerarlos todos. Por esta razón, es muy importante que el lector compile y ejecute todos los ejemplos presentados en este libro. Uso de Paquetes.
El programa presentado en la sección anterior consta de dos clases: La clase Trabajador y la clase Main. Estas dos clases deben de residir en el mismo directorio, de otra forma, el programa no compilará pues la clase Main hace referencia a la clase Trabajador y sólo las clases que están en el mismo directorio pueden ser “vistas” “vistas” por otras clases. Esto obligaría a los programadores a crear todas las clases en el mismo directorio. La siguiente lista enumera los problemas que esta restricción trae en el desarrollo de un programa no trivial: tr ivial:
Los programas quedan mal estructurados pues todas las clases deben estar en el mismo directorio, aún si no están relacionadas. En el desarrollo de programas complejos, es probable que varios programadores colaboren en el mismo proyecto. Esto genera la posibilidad de que haya duplicidad en el nombre de las clases. Si no se utilizan paquetes, es imposible manejar este conflicto. Todas las clases pueden acceder a todos los atributos de las otras clases. Esto es indeseable y potencialmente peligroso (más sobre eso después)
Java utiliza paquetes para eliminar los problemas mencionados en la lista de arriba. Un paquete es simplemente un directorio o folder en el cual se agrupan las clases que están relacionadas. Al usar paquetes, un programador puede colocar las clases relacionadas en cierto paquete e importarlas para uso en otros paquetes. Por ejemplo, supongamos que tenemos pensado pensado que el programa de nómina que hemos desarrollado sufrirá cambios en el futuro (lo cual es bastante probable) para acomodar nuevas necesidades de la empresa, como son:
Incorporación de nuevos tipos de trabajadores Conexión a una sucursal remota para el cálculo cálculo del pago de los trabajadores de esa sucursal Conexión a las computadoras del gobierno para el envío de declaraciones de impuestos en formato electrónico Interfaz visual con los usuarios Etc.
Como puede el lector imaginarse, estos cambios harán que el número de clases requeridas por el programa crezca de manera importante y es evidente que será necesaria la agrupación de estas clases en paquetes. paquetes. Por lo pronto, crearemos crearemos dos paquetes. paquetes. Un paquete paquete que llamaremos llamaremos Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
32
“nomina” porque en este paquete colocaremos las clases relativas a la nómina (note que eliminamos el acento en la palabra nómina para evitar algún problema con el sistema operativo en el que se ejecute el programa). La única clase que colocaremos por ahora en este paquete es la clase Trabajador. El otro paquete que crearemos, agrupará las clases que no pertenecen estrictamente al dominio de la nómina, pero que son necesarias para la ejecución del programa. En este caso, la clase Main. Llamaremos a este paquete “ etc” (por etcétera). La figura 2.11 muestra las clases y los paquetes a los que pertenecen. A este tipo de diagramas se les llama “Diagrama de Clases”. Veamos varios puntos interesantes del diagrama. Lo primero que observamos es que los paquetes se representan como carpetas o “folders” y las clases se representan con un rectángulo. Esta notación se utiliza en UML (Unified Modeling Language) o “Lenguaje Unificado de Modelado”. Este lenguaje, utilizado extensivamente en la industria y en la academia, permite representar procesos y sistemas. Es un lenguaje muy extenso que incluye muchos símbolos y tipos de diagramas. La figura 2.10 presenta un D iagrama de Clases. Este tipo de diagramas permiten representar las clases, con sus atributos y métodos y los paquetes a los cuales pertenecen.
Figura 2.11 Diagrama de Clases del Programa de Nómina La siguiente lista contiene los puntos más relevantes del Diagrama de Clases:
Los paquetes se representan por carpetas (folders) Las clases se representan por rectángulos La parte superior del rectángulo de la clase contiene el nombre de la clase La parte de en medio del rectángulo de la clase contiene los atributos de los objetos de esa clase.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
33
La parte inferior del rectángulo de la clase contiene los métodos de los objetos de esa clase.
El símbolo se utiliza para indicar que la clase pertenece al paquete. Aunque también se puede dibujar la clase adentro del paquete. Los métodos estáticos se representan con las letras subrayadas. En este caso, el método main() de la clase Main es estático y por eso se escribe con letras subrayadas. Seguramente, el lector se estará preguntando ¿Qué es un método estático? Lo veremos un poco más adelante en este capítulo.
Regresemos ahora a código en Java que implementa el diagrama de la figura 2.11. Lo primero que debemos hacer es crear dos subdirectorios: nomina y etc los cuales estarán situados abajo del directorio ejemploNomina. Posteriormente colocaremos los archivos Trabajador.java y Main.java en los directorios correspondientes. La figura 2.12 muestra la estructura de los directorios y los archivos.
Figura 2.12 Directorios correspondientes a los paquetes de la figura 2.11 Los cambios que debemos hacer a los programas que habíamos mostrado anteriormente son mínimos. Las figuras 2.13 y 2.14 muestran los listados completos de ambos archivos.
Figura 2.13 Código de la clase Trabajador incluida en el paquete nomina del Programa de nómina La figura 2.13 muestra el código de la clase Trabajador. Hemos realizado sólo dos cambios con respecto al código de la figura 2.6. El primer cambio se observa en la primera línea, en la cual se incluyó la declaración del paquete. Para declarar que una clase está incluida en un paquete es necesario escribir al principio del archivo la palabra package seguida del nombre del paquete. Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
34
Así, package nomina; en la primera línea del archivo Trabajador.java indica que la clase Trabajador pertenece al paquete nomina. El segundo cambio es en la línea 3. En esa línea se agregó la palabra public a la declaración de la clase. Este segundo cambio es necesario porque la clase Trabajador es utilizada en otra clase (Main) que no está en el mismo paquete. Como regla general, podemos decir que si deseamos utilizar una clase en otro paquete, esta clase deberá ser pública. La figura 2.14 muestra el código de la clase Main que se encuentra en el paquete etc. Como puede observarse, se agregaron dos líneas con respecto al código de la figura 2.8. Agregamos la declaración del paquete en la línea 1 y además importamos la clase Trabajador en la línea 2. Esto último requiere una explicación más detallada. En ocasiones, deseamos utilizar una clase que no se encuentra en el mismo paquete que la clase que estamos escribiendo. En nuestro ejemplo, la clase Trabajador no está en el mismo paquete que la clase Main. En este caso, la clase que desea utilizar una clase de otro paquete, debe incluir en su código la instrucción import, La nomina.Trabajador; le indica a la computadora que la clase Main desea línea: import nomina.Trabajador; utilizar la clase Trabajador, la cual se encuentra en el paquete nomina. Observe que se utiliza un punto para separar el paquete del nombre de la clase. La compilación y ejecución de este programa se realiza desde el directorio ejemploNomina. Como puede verse en la figura 2.15, tanto para la compilación, como para la ejecución, se utiliza el nombre completo de la clase, es decir, el paquete al que pertenece y el nombre de la clase. Observe también que no es necesario compilar la clase Trabajador.java. Sólo de compila el código de la clase Main.java y como este código hace referencia a la clase Trabajador, el compilador busca a esta clase en el paquete correspondiente y realiza la compilación de manera automática.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
35
Figura 2.14 Código de la clase Main incluida en el paquete etc del Programa de Nómina
Figura 2.15 Resultado de la compilación y ejecución del programa de nómina. El uso de paquetes permite estructurar los programas complejos de tal forma que las clases que estén relacionadas puedan ser agrupadas en un paquete. Por ejemplo, si un programador desea utilizar interfaces gráficas en sus programas (ventanas, menús, cuadros de textos, etc.) Puede Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
36
incorporar a su programa las clases que estén en ese paquete. Pero si además desea que su programa se conecte a una red, podría incorporar al programa el paquete que contiene las clases necesarias para la conexión a redes. De igual forma podríamos incorporar (importar) clases para el manejo de archivos, impresoras, páginas web, etc. Además de lo anterior, el uso de paquetes permite que grupos de programadores escriban programas sin tener que preocuparse por el nombre que le den a las clases siempre y cuando no nombren igual a los paquetes. Por ejemplo, supongamos que un programador de una compañía que desarrolla videojuegos está escribiendo las clases para la creación de naves. Decide entonces nombrar a su paquete “naves”. Dentro del paquete naves, decide crear una clase Casco (para crear los cascos de los barcos). Otro programador de la misma compañía, es el encargado de crear las clases necesarias para los personajes del videojuego decide crear un paquete que contenga todas las clases para la creación de los soldados del videojuego. A este paquete le llama “soldados”. Dentro del paquete “soldados” coloca la clase “Casco” para referirse referirse a los cascos de los soldados. Como puede verse, cada uno de los programadores han creado una clase llamada “Casco”. Aunque se llaman igual, ambas clases corresponden a objetos muy distintos y se encuentran en diferente paquete. En el caso que se requiera el uso de ambas clases en un programa, se deberán referenciar con el nombre completo del paquete y la clase. Para este ejemplo, las clases para el casco del barco y el casco del soldado se harían referencia con los nombres: naves.Casco y soldados.Casco respectivamente. La figura 2.16 muestra el código de la clase Videojuego que utiliza ambas clases.
Figura 2.16 Código de la clase Videojuego que utiliza dos clases que se llaman igual pero localizadas en diferentes paquetes Observe que el código de la figura 2.16 no contiene la instrucción “ import” para las clases naves.Casco y sodados.Casco. De hecho, si se incluyeran las instrucciones para importar ambas clases, el compilador marcaría un error por la ambigüedad que representa importar dos clases con el mismo nombre. Es por esta razón que en las líneas líneas 3,4, 6 y 7 se hace referencia al nombre completo de la clase (esto es, incluyendo el nombre del paquete).
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
37
Resumen del Capítulo.
En este capítulo creamos, compilamos y ejecutamos nuestros primeros programas en Java. Vimos que para poder ejecutar un programa que utiliza objetos, es necesario previamente crear las clases que definen las características y funciones de los objetos. Posteriormente, es necesario crear los objetos y finalmente enviar mensajes a los objetos para que efectúen sus funciones. Vimos la forma en que se declaran los atributos y los métodos de los objetos y cómo se llaman a sus métodos. Finalmente utilizamos paquetes para agrupar las clases relacionadas en directorios y estudiamos ejemplos de uso de paquetes en el código. Ejercicios y preguntas.
1. Explique brevemente el concepto de abstracción. 2. Describa las principales diferencias entre la Programación Orientada a Objetos y la Programación Imperativa. 3. Considere el siguiente enunciado: “El Comité Comité Olímpico Olímpico Internacional desea tener tener en su computadora la información correspondiente a los atletas que participan en la Olimpiada. Se desea producir información sobre el desarrollo de los juegos. Ejemplos de esta información es la siguiente: Gafetes de los atletas y entrenadores, cuadro de Medallas por país, resultados de cada una de las competencias, etc.”. etc. ”. De acuerdo al enunciado, identifique las clases con los atributos y métodos de los objetos que de acuerdo a su opinión sean los más relevantes. 4. Escriba un programa en Java que resuelva el siguiente problema. Un distribuidor de Automóviles desea almacenar en su computadora los datos importantes (marca, modelo y número de asientos) de los vehículos que vende. Escriba la clase Automovil de tal forma que el siguiente código pueda compilar y ejecutar sin errores. public class Main { public static void main(String[] args) { Automovil a = new Automovil(); a.marca = “Ford”; a.modelo = 2000; a.numeroDeAsientos a.numeroDeAsientos = 4; 4; System.out.println("Se System.out.println("Se ha creado un automovil marca:"+a.getMarca()); System.out.println("Con System.out.println("Con "+a.getNumeroDeAsientos()+" "+a.getNumeroDeAsientos()+" asientos"); System.out.println("El System.out.println("El cual tiene "+a.calculaAntiguedad()+" "+a.calculaAntiguedad()+" años de uso"); } } La deberá ser la siguiente: Se ha creado un automóvil marca: Ford Con 4 asientos El cual tiene 12 años de uso 7. Describa las ventajas que tiene el uso de paquetes en programas complejos.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
38
8. De la siguiente lista de nombres de atributos y métodos, identifique aquellos que sean incorrectos de acuerdo a la notación estándar de Java y escriba la razón por la que son incorrectos a. salariodiario e. salarioDiario i. calculasalario()
b. Salariodiario f. salario j. calculaSalario()
c. salario diario g. SalarioDiario k. calcula el salario()
d. salario_diario h. salario/diario l. CALCULASALARIO()
9. Dibuje un Diagrama de Clases que represente el nombre de la clase, los atributos y métodos del siguiente código:
10. Escriba el código en Java que represente las clases correspondiente al siguiente diagrama de clases:
Para implementar el método: calculaEdad() de la clase escuela, se debe restar al año actual el año de nacimiento del alumno. Puede probar su código compilando y ejecutando el código mostrado a continuación:
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
39
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
40
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
41
3. Conceptos Básicos de Programación Orientada a Objetos En este capítulo veremos diversos temas importantes para el desarrollo de programas orientados a objetos. La razón por las que se incluyeron estos temas en un capítulo aparte es simplemente didáctica. Con el conocimiento de los conceptos estudiados en el capítulo anterior, el estudiante debe ser capaz de escribir programas en Java que definan clases y creen objetos que realicen funciones simples. Sin embargo, apenas estamos empezando. La intención del autor es que el estudiante realice los ejercicios conforme avanza en la lectura y es por esa razón que se escogió hacer más pequeños los capítulos. Con el conocimiento de los nuevos conceptos que se introducirán en este capítulo, el estudiante podrá escribir programas más complejos que representen más adecuadamente la parte del mundo real que se pretende modelar. Como se mencionó anteriormente, el objetivo de la programación orientada a objetos es el modelado del mundo real a través del proceso de abstracción. Esto es, tomando en cuenta sólo las características del mundo real que nos interesa modelar. El mundo real es muy complejo y por consiguiente, muchos conceptos que suceden en el mundo real deben de poderse representar de alguna forma dentro de la programación orientada a objetos. Algunos de los conceptos importantes que veremos en este capítulo son: Métodos y atributos de clase, constructores, arreglos y métodos de acceso a los miembros (atributos y métodos) de los objetos. Métodos y Atributos de Clase.
En el capítulo anterior vimos que los objetos tienen comportamiento (métodos) y estado (atributos). También hicimos una analogía con un molde utilizado para hacer un pastel. El molde corresponde a la clase y el pastel es el objeto. Pero… una pregunta que podemos hacernos es : ¿El molde en sí, podría tener sus propios atributos y métodos? Pensemos por un momento en el molde. La respuesta por supuesto es si. Ahora… ¿Podría identificar el lector atributos propios del molde? Por ejemplo, el material del molde, las dimensiones, el color, etc. Todos estos atributos pertenecen al molde y no al pastel. Es muy importante que el lector entienda perfectamente esta idea antes de continuar. Lo que se acaba de afirmar es que las clases (moldes) también pueden tener sus propios atributos y métodos. Entonces tenemos dos clases de atributos (y métodos) los que pertenecen a los objetos y los que pertenecen a las clases. En muchas ocasiones en nuestra actividad de programación, encontraremos la necesidad de utilizar métodos y atributos de la clase. La figura 3.1 muestra algunos atributos de clase y de objeto para el ejemplo del molde para hacer pasteles. La pregunta que ahora puede hacerse el lector es: ¿Por qué complicarse la vida con esto de los atributos y métodos de clase? ¿No era suficiente con los atributos y métodos de los objetos? La respuesta es desafortunadamente, no. Los atributos y métodos de los objetos son útiles para Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
42
representar características y comportamiento de los objetos solamente. Imagine el lector la siguiente situación. En el programa para el cálculo de la nómina que realizamos en el capítulo anterior, un nuevo requerimiento es conocer el número de trabajadores que tiene la empresa. La pregunta que podemos hacernos es ¿Se puede considerar al número de trabajadores como un atributo del objeto trabajador? Si razonamos un poco esta pregunta nos daremos cuenta que no es correcto considerar al número de trabajadores como un atributo del objeto trabajador pues no proporciona información acerca del trabajador. Entonces… ¿Será un atributo de la clase? ¡Exactamente! El número de trabajadores es un atributo de la clase Trabajador pues representa información acerca de la clase completa y no sólo de un individuo.
Figura 3.1 Atributos y Métodos de Clase y del Objeto Los atributos y métodos de los objetos reciben también el nombre de atributos y métodos de instancia. En java, para representar un atributo ó método de clase se utiliza la palabra reservada static. Por esta razón, algunos programadores le llaman a este tipo de atributos y métodos estáticos. Vamos ahora a utilizar este conocimiento para incluir un nuevo requerimiento al programa de nómina del capítulo anterior. Supongamos que nos solicitan ahora que contemos el número de trabajadores que laboran en la empresa. La salida requerida del programa se muestra en la figura 3.2
Figura 3.2 Salida deseada del programa de nómina
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
43
Como puede observarse, el nuevo requerimiento consiste en contar el número de trabajadores de la empresa y mostrarlo al final. La forma como resolveremos este nuevo requerimiento es la siguiente. Usaremos un atributo estático de la clase Trabajador al que llamaremos numTrabajadores. Ahora… ¿Cómo lo vamos utilizar para contar el número de trabajadores? Utilizaremos un constructor . Un constructor es algo parecido a un método (debe quedar quedar claro que un constructor no es un método) que se ejecuta cuando se crea un objeto. A diferencia de los métodos, los constructores no regresan ningún valor y además sólo pueden ejecutarse una vez, al momento de la creación del objeto. Los constructores se deben llamar exactamente igual que la clase y no se declara el valor de retorno. La figura 3.3 muestra el código de la clase Trabajador. En el código se muestra la declaración del atributo de clase (estático) numTrabajadores y también el código del constructor de la clase Trabajador. Como puede observarse en la figura 3.3, se declara el atributo de la clase numTrabajadores en la línea 4. La forma en que se declara un atributo estático (o de clase) es muy parecida a la forma de declarar un atributo de instancia, simplemente escribimos la palabra “ static” antes del tipo de datos y el lenguaje Java lo interpreta como un atributo que pertenece a la clase Trabajador y no al objeto de dicha clase.
Figura 3.3 Código de la clase Trabajador con su constructor y su atributo de clase Aunque la diferencia en la declaración de los atributos de clase y de instancia es mínima (sólo una palabra) la diferencia en significado es muy grande. Como se mencionó anteriormente, los atributos de instancia pertenecen al objeto y por lo tanto se almacenan en la memoria heap. Por otro lado, los atributos de clase no pertenecen al objeto sino a la clase y por lo tanto no se almacenan en la misma área. Como los atributos de la clase no requieren de ningún objeto para poder utilizarse, se puede acceder a ellos simplemente a través del nombre de la clase. Esto queda ejemplificado en la línea 10 de la figura 3.3 pues estamos incrementando en uno la variable Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
44
estática numTrabajadores de la clase Trabajador sin tener que hacer referencia a ningún objeto. Las líneas 9 a 11 corresponden al constructor de la clase Trabajador. Como se mencionó anteriormente, estas líneas se ejecutan cuando se crea cada objeto de la clase Trabajador. De esta manera, nos estamos asegurando de que se “cuente” cada trabajador creado. En la En la figura 3.4 se presenta el código de la clase Main que crea los trabajadores y muestra su pago considerando el nuevo requerimiento de mostrar el número de trabajadores. El código de la figura 3.4 es prácticamente el mismo código que el de la figura 2.14. Excepto por la línea 33. Esta línea muestra el número total de trabajadores de la empresa. Como puede observarse, la forma de acceder al atributo de clase desde afuera de la clase es idéntica a la forma como accedimos al atributo desde adentro de la clase (línea 10 de la clase Trabajador). El lector podría hacerse la siguiente pregunta. ¿Si los atributos de clase no requieren la creación de objetos para su uso… entonces en qué momento se pueden utilizar? La respuesta es: desde el primer momento, es decir, desde que se inicia la ejecución de un programa. Si la clase no está en el mismo paquete, entonces desde que se importe la clase. De hecho, una clase puede ser accedida aún sin importarla, si se utiliza el nombre completo de la clase incluyendo el nombre del paquete como lo usamos en el ejemplo de la figura 2.16 Constructores
En el ejemplo anterior, utilizamos el constructor de la clase Trabajador para “contar” cuantos trabajadores se han creado en la empresa. Los constructores son una especie de métodos que se ejecutan únicamente cuando se crean los objetos. Por ejemplo, cuando se ejecuta la instrucción: Trabajador t1 = new Trabajador();
Se realizan los siguientes pasos: 1. Se reserva un espacio en la memoria heap para el nuevo objeto de la clase Trabajador
2. Se ejecuta el constructor de la clase Trabajador 3. Se asigna a variable t1 la dirección de memoria en la cual se creó el nuevo objeto En este ejemplo, utilizamos el constructor para incrementar el contador de trabajadores. Un uso más común de los constructores es para inicializar valores de los atributos de los objetos. En el ejemplo de la figura 3.4 se asignan los valores de los atributos de los objetos en las líneas: 6, 7, 8, 10, 11, 12, 14,15, 16. Esto en general no es una buena práctica de programación, en primer lugar porque es necesario escribir mucho código, en segundo lugar porque no es una buena práctica de programación permitir que los atributos de las clases sean modificados desde afuera de la clase.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
45
En este ejemplo, desde la clase Main hemos asignados los valores a los atributos de instancia de los objetos de la clase Trabajador.
Figura 3.4 Código de la clase Main incluida en el paquete etc del Programa de Nómina Modificaremos ahora el código de la clase Trabajador de tal forma que podamos inicializar (enviar valores iniciales) a los objetos para que, justo después de haberse reservado en la memoria heap el espacio para el objeto, se asignen los valores de sus atributos. Para lograr lo anterior, vamos a utilizar un constructor que permita recibir los parámetros que contienen los valores de los atributos de los objetos. Por supuesto, también contaremos a los trabajadores como lo hicimos anteriormente. La figuras 3.5 y 3.6 muestran los códigos de la clase Trabajador y de la clase Main respectivamente.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
46
Figura 3.5 Código de la clase Trabajador cuyo constructor recibe valores de atributos La diferencia entre el código de la figura 3.5 y el de la figura 3.3 es precisamente en el constructor que se encuentra en las líneas 9 a 14 de la figura 3.5. Podemos ver que ahora el constructor recibe tres parámetros (nombre, salarioPorHora y horasTrabajadas). Estos parámetros son variables que toman su valor cuando se hace el llamado al constructor (o sea cuando se crea el objeto). La línea 10 contiene la asignación: this.nombre = nombre; la palabra “this” se utiliza en este caso para resolver una ambigüedad pues tenemos dos variables con el mismo nombre… ¿Cuáles? ¿Cuáles? La variable “nombre” (que en este caso es más bien atributo) que fue definida en la línea 5 y la variable “ nombre” que fue definida como un argumento del constructor de la clase Trabajador en la línea 9. La palabra “this” hace que la parte izquierda de izquierda de la asignación se refiera al atributo. De esta forma, estamos asignando el valor del parámetro “ nombre” al atributo “nombre”. Podíamos haber prescindido del uso de la palabra “ this” si hubiéramos usado otro nombre para el parámetro del constructor pero escogimos hacerlo así precisamente para mostrar el uso de esta palabra. La palabra “this” es muy utilizada en java pues es la forma que tenemos de hacer referencia a un objeto dentro de sus métodos. Dicho sea de otra forma, el “this” de Java es análogo al “yo” (o al “mi”) que utilizamos en nuestro lenguaje cotidiano. Si estamos haciendo referencia a nosotros mismos, utilizamos la palabra “yo” o “mi”. En la línea : this.nombre = nombre;
Lo que estamos expresando es: “Asigna el valor que tenga el parámetro ‘nombre’ que aparece en la lista de parámetros del constructor a mi atributo ‘nombre’”
Similarmente, las líneas 11 y 12 asignan las horas trabajadas y el salario por hora a los atributos respectivos del objeto. Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
47
Figura 3.6 Código de la clase Main que utiliza el constructor de la clase Trabajador para asignar los valores iniciales de los atributos Las líneas 5, 6 y 7 del código de la clase Main utilizan el constructor de la clase Trabajador. Como puede observarse, se disminuye considerablemente considerablemente la cantidad de líneas de código. Además de lo anterior, esta forma de asignar valores iniciales a los atributos tiene varias ventajas importantes que veremos más adelante. Sobrecarga de Constructores C onstructores
Una de las características más útiles de la programación orientada a objetos es la sobrecarga de constructores. Esta consiste en definir varios constructores de la misma clase pero con diferentes parámetros. Supongamos por ejemplo, que la mayoría de los trabajadores trabajan 40 horas semanales. Entonces podríamos tener un constructor especial para estos trabajadores. Este constructor asignaría de manera “automática” el valor 40 al atributo horasTrabajadas. Las figuras 3.7 y 3.8 muestran los códigos de las clases Trabajador y Main respectivamente. respectivamente.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
48
Figura 3.7 Clase Trabajador con el constructor sobrecargado Las líneas 14, 15 y 16 muestran el constructor de la clase Trabajador sobrecargado. Como puede observarse en las líneas 8 y 14 se declaran dos dos constructores de la clase Trabajador, lo cual es perfectamente válido siempre y cuando los parámetros sean diferentes ya sea en tipo o en cantidad. En este caso, los parámetros son diferentes en cantidad pues en la línea 8, el constructor espera 3 parámetros mientras que en la línea 14 el constructor sólo recibe dos parámetros. En la línea 15, encontramos otro uso de la palabra “ this”. En este caso, usamos la palabra “this” para llamar al constructor con tres argumentos, es decir, al constructor definido en la línea 8. De esta manera, cuando se llama al constructor con dos argumentos (línea 14), este constructor llama al constructor con tres argumentos (línea 8). El argumento que se “rellena” es precisamente el de las horas trabajadas, el cual es una constante con valor de 40. Algo que es muy importante señalar es que si se utiliza la palabra “this” para llamar otro constructor, este llamado debe ser la primera instrucción del constructor . De lo contrario, el programa no compilará, puede el lector contestar la siguiente pregunta. ¿Cuál sería el error en el siguiente código? class X { X(int a){ this(5); } }
La línea 5 de la clase Main de la figura 3.8 crea el objeto de la clase Trabajador con dos argumentos. Observe que las líneas 6 y 7 también crean objetos de la clase Trabajador pero con tres argumentos. Java puede distinguir cual constructor utilizar de acuerdo a número de parámetros. No es difícil encontrar clases que contienen varios constructores para manejar las diferentes situaciones que se pueden presentar al momento de construir un objeto.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
49
Figura 3.8 Código de la clase Main que utiliza el constructor sobrecargado de la clase Trabajador
Arreglos
En muchas ocasiones, es conveniente almacenar un grupo de objetos “dentro” de otro objeto con el fin de poder acceder a ellos y manipularlos de una manera conveniente para el programador. A los objetos contenidos en el arreglo se les llama elementos del arreglo. Por ejemplo, imagine que la empresa a la que estamos calculando la nómina tuviera más de 3 empleados, digamos… digamos… unos 10. ¿Puede imaginarse el lector como se complicaría el código de la figura 3.8? Tendríamos que repetir las líneas 5,6 y 7 y las líneas 10 a 23 varias veces más, una por cada una de los 10 trabajadores. Por esta razón, se crearon los arreglos en los lenguajes de programación. Un arreglo es un objeto que “contiene” a un grupo de objetos (aunque también puede contener valores simples como enteros, caracteres, etc.). Con el uso de arreglos, estas repeticiones se pueden poner dentro de ciclos, como se verá un poco más adelante. Los arreglos no forman parte exclusiva de la programación orientada a objetos, es decir, otras formas o paradigmas de programación programación también permiten permiten manejar arreglos. arreglos. Sin embargo, es un tipo de objetos muy utilizado en la programación orientada a objetos. La siguiente lista muestra las características principales de los arreglos: 1. Los arreglos son objetos 2. Los arreglos tienen un tamaño fijo que se define en el momento de crear el arreglo 3. Todos los elementos del arreglo deben ser del mismo tipo 4. Los elementos se acceden a través de un índice numérico que empieza en 0
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
50
5. El último elemento del arreglo está en la posición t-1 donde t es el tamaño del arreglo
Declaración y Creación de arreglos.
Como se mencionó anteriormente, todos los arreglos son objetos, por lo que residen en la memoria heap. La forma de declarar un arreglo es muy parecida a la declaración de una variable o atributo, sólo que se agregan los corchetes ‘[]’ para indicar que se trata de un arreglo. Por ejemplo, la declaración: int[] a;
Declara que la variable (o atributo) a es un arreglo que contendrá un cierto número de enteros. Note que como todavía no se está creando el arreglo, no es necesario todavía especificar la cantidad de elementos (el número de números enteros) que serán almacenados en el arreglo. También podemos tener arreglos de objetos. Por ejemplo: Trabajador[] trabajadores;
Declara un arreglo de trabajadores. Igualmente a la declaración anterior, no es necesario determinar cuántos cuántos trabajadores trabajadores contendrá contendrá el arreglo trabajadores porque todavía no se está creando el arreglo. Las siguientes instrucciones crean ambos arreglos respectivamente: a = new int[15]; trabajadores = new Trabajador[10];
Estas dos últimas instrucciones crean los arreglos a y Trabajadores. El arreglo a tiene 15 enteros y el arreglo trabajadores tiene 10 objetos de la clase Trabajador. Al momento de crear los arreglos, todos los elementos del arreglo a serán 0’s y los elementos del arreglo trabajadores contendrán un valor especial llamado “null” que representa que el objeto no ha sido creado. Es importante hacer la siguiente aclaración: Cuando se crea un arreglo que contiene objetos (como el arreglo trabajadores), sólo se crea un objeto (o sea… ¡el sea… ¡el arreglo!) y no los objetos que contiene. Debe quedar claro que la instrucción: trabajadores = new Trabajador[10];
Sólo crea un objeto y no 10 como erróneamente se puede suponer. El único objeto creado es el arreglo en si. Aunque este arreglo tiene 10 espacios para los elementos, estos espacios están vacios, o sea, tienen un valor de “null”. La figura figura 3.9 muestra el estado de la memoria después de crear el arreglo trabajadores.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
51
Como puede observarse en la figura 3.9, el arreglo trabajadores ha sido creado en la memoria heap. Sin embargo, los 10 espacios reservados para cada uno de los trabajadores contienen la palabra “null”. Esto significa que los espacios están en blanco. Sabemos que cada uno de esos espacios contendrá un objeto de la clase Trabajador pero hasta este momento no se han creado estos objetos.
Figura 3.9 Estado de la memoria Heap después de haber creado el arreglo trabajadores También es posible hacer la declaración de la variable y crear el arreglo al mismo tiempo. Por ejemplo, la línea: Trabajador [] trabajadores = new Trabajador[10];
Declara el arreglo trabajadores y además crea el arreglo con 10 posiciones en la memoria heap. El resultado es exactamente el mismo que si se usa el par de líneas: Trabajador [] trabajadores; trabajadores = new Trabajador[10];
Ahora que ya se tiene el arreglo creado, el paso necesario para poder almacenar a los t rabajadores en el arreglo es crear cada uno de los 10 trabajadores y asignarlo a cada una de las 10 posiciones del arreglo. Existen varias formas de hacer lo anterior. Primero presentaremos la forma estándar y luego presentaremos la forma abreviada. Las figuras 3.10 y 3.11 muestran ambas formas respectivamente.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
52
Figura 3.10 Forma “estándar” de creación y carga del arreglo trabajadores En la figura 3.10 la línea 8 declara la variable trabajadores como un arreglo de objetos de la clase Trabajador, el cual contiene 10 elementos. También se crea el arreglo en la memoria heap. Sin embargo, no se crean ninguno de los 10 objetos de la clase Trabajador. La línea 9 crea el objeto que corresponde al trabajador “Armando Paredes” y lo asigna al elemento 0 del arreglo trabajadores. Las líneas 10 a la 18 crean los demás objetos de la clase y los asignan a las posiciones 1 a la 9. El tamaño del arreglo es de 10 posiciones, es por esta razón que el último elemento del arreglo está en la posición 9. Si el programa intenta acceder a un elemento en una posición mayor a 9 (o menor a 0), se generará un error de ejecución. La figura 3.11 muestra otra forma más compacta o abreviada de crear y asignar valores a un arreglo. A diferencia de la forma anterior, esta forma abreviada no requiere especificar el número de elementos del arreglo. Como puede observarse en la línea 8, se asigna directamente al arreglo los objetos creados, los cuales se encierran en llaves. ¿Cómo determina entonces el compilador el tamaño del arreglo? Se deja al lector que encuentre la respuesta.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
53
Figura 3.11 Forma “abreviada” de creación y carga del arreglo trabajadores Las dos formas de declarar, crear y asignar valores al arreglo presentadas en las figuras 3.10 y 3.11 son idénticas. Es decir producen producen exactamente el mismo resultado. resultado. El estado de la memoria heap después de la ejecución de cualquiera de los dos códigos se muestra en la figura 3.12. Podemos observar fácilmente en la figura 3.12 que después de la ejecución de cualquiera de los códigos mostrados en la figura 3.10 y 3.11 se habrán creado 11 objetos en la memoria heap. ¿Porqué 11? Porque se crearon 10 objetos de la clase Trabajador y un objeto que contiene a estos 10 objetos. En realidad los 10 objetos no están “adentro” del arreglo como puede observarse en la figura 3.12. Cada elemento del arreglo contiene un “apuntador” que indica la posición dentro de la memoria heap en la cual se encuentra almacenado el objeto. Por ejemplo, la posición 0 del arreglo trabajadores contiene un apuntador (o dirección de memoria) que corresponde a la dirección de memoria en la cual está almacenada toda la información del objeto “Armando Paredes”. En la figura 3.12 no se incluyeron los demás atributos del objeto por razones de espacio. En resumen, hemos visto que un arreglo es un objeto que a su vez puede contener varios objetos. La forma como accedemos a cada uno de los objetos contenidos en el arreglo es por medio de un índice. Este índice comienza en 0 que corresponde a la primera posición y termina en la posición que se calcula restando uno al tamaño del arreglo. Otra cosa importante que vale la pena remarcar es que al crear el arreglo no se crean los elementos del mismo. Esto hace necesario que además de crear el arreglo se deban crear cada uno de los elementos del
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
54
mismo. En este libro usaremos extensivamente los arreglos para almacenar información de varios objetos (o tipos primitivos de datos) en otro objeto.
Figura 3.12 Estado de la memoria heap después de crear los trabajadores y asignarlos a las posiciones del arreglo A pesar de haber visto las ventajas del uso de arreglos, es aún posible que algún lector suspicaz piense: piense: ”Bueno… pero… ¿Donde está el ahorro de código?” De código?” De todas formas, se tienen que crear los objetos y al final se está utilizando una línea de código por cada trabajador. Lo mismo que se hace en el código de la figura 3.8. Es cierto. El ahorro en el código no está en la declaración del arreglo sino en el procesamiento del mismo. Es decir, en el código que calcula e imprime la nómina. La figura 3.13 muestra el código completo del programa.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
55
Las líneas 23 a 28 de la figura 3.13 realizan la misma función que las líneas 11 a 23 de la figura 3.8. Esto es, ¡Un ahorro de 7 líneas! Pero esto es porque sólo aumentamos 7 trabajadores más. Imagine por favor el lector si en lugar de 7, hubiéramos aumentado el número de trabajadores a 500 (o a 5000). El ahorro en líneas de código sería muy grande.
Figura 3.13 Código completo del programa de Nómina utilizando un arreglo
La línea 23 de la figura 3.13 inicia el ciclo “for” que termina en la línea 28. La variable i declarada en la línea 23 se utiliza como como índice para acceder a los elementos del arreglo trabajadores. De esta forma, se calcula el pago y se muestra el sueldo por cada trabajador. Existe aún una pregunta que algún lector suspicaz pudiera hacer con respecto al ahorro de código. ¿Qué hay de las líneas 5 a la 15? ¿Por qué no podemos utilizar un ciclo para ahorrarnos líneas de código? El problema es que tenemos que “cargar” el arreglo de trabajadores con los datos de cada uno de ellos. Sería imposible hacerlo en un ciclo, a menos que asignáramos los mismos valores a todos los trabajadores, o…utilizáramos archivos para almacenar la información, lo cual haremos casi el final de este libro en el capítulo que trata sobre archivos y flujos de datos. Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
56
Algo que vale la pena mencionar, es que hemos hemos cambiado varias veces el código de la clase Main sin necesidad de alterar la definición de la clase Trabajador. Esto es una característica muy importante de un buen diseño. Tratar de evitar al máximo que una modificación de una clase afecte al diseño de otras. Este concepto se llama “bajo acoplamiento” o “low coupling”. coupling” . En general, se debe diseñar cada clase de tal forma que sólo tenga atributos y métodos relativos y pertenecientes exclusivamente a esa clase. Por ejemplo, supongamos que para el pago de la nómina es necesario el uso de un tabulador que indique la tarifa correspondiente al trabajo que desempeñó el trabajador. Si un trabajador trabaja como soldador recibe un salario de 100 pesos por hora pero si trabaja como lubricador lubricador entonces recibe 150 pesos la hora. Entonces, podríamos estar tentados a incorporar la tarifa a la clase Trabajador. Sin embargo, ésta no sería una buena decisión de diseño pues se tendría que modificar la clase Trabajador si existe algún cambio en las tarifas. Lo conveniente en este caso es crear otra clase que podríamos llamar Tarifa que contiene exclusivamente la información de las tarifas y almacenar en cada objeto de la clase Trabajador las horas trabajadas y el tipo de trabajo desempeñado. Permisos de Acceso a los Miembros de los O bjetos.
Una de las características más importantes de la programación orientada a objetos es la posibilidad de “esconder” información de los objetos al mundo exterior. ¿Por qué necesitamos esconder información de los objetos? Hay varias respuestas posibles. Empezaremos con un ejemplo. Imaginemos que tenemos un piano como el que se muestra en la figura 3.14
Figura 3.14 Piano con sus interfaces pública y privada
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
57
En la figura 3.14 podemos observar un piano de cola. cola. En el piano distinguimos distinguimos las teclas y los pedales como su interface con el pianista (usuario normal de un piano). A esta interface le llamamos interface pública porque esta interface está completamente expuesta, o sea al alcance de cualquier usuario. El usuario puede interactuar con el piano aunque no sepa tocarlo sin que pase nada malo. ¡Excepto quizás, un mal rato para los desafortunados que tengan que oír la ejecución! Por otro lado, tenemos las perillas de ajuste que se utilizan para la afinación del piano. Estas perillas sólo debe moverlas un especialista en afinación de pianos. Este oficio es complicado y no cualquiera puede llevarlo a cabo. Las perillas de afinación les llamamos interface privada porque no cualquiera debe tener acceso a ellas. Podemos decir que están escondidas debajo de una tapa y por esta razón no están a la vista de los usuarios normales. Para acceder a ellas, es necesario desmontar la tapa que las cubre. Si un usuario inexperto mueve estas perillas, el piano quedaría desafinado. Esto es algo muy grave, y es por esa razón que las perillas de ajuste están ocultas para los usuarios normales. Existe otra razón igual de importante para separar las interfaces públicas y privadas de los objetos. De nuevo, recurriremos al ejemplo del piano para ilustrar este concepto. Supongamos que el fabricante de pianos decide modificar el funcionamiento interno del piano. Esto implica un cambio en la forma de afinación de las perillas. Imagine el lector lo que pasaría si este cambio en el funcionamiento del piano ¡afectara también la forma de tocarlo! ¡Esto sería un desastre! Ningún pianista desearía tocar un piano que requiera habilidades especiales. Por esta razón, los fabricantes de piano no modifican la interface pública (teclas, pedales). Sin embargo, pueden modificar la interface privada sin mayores consecuencias pues los afinadores de los pianos pueden adaptarse más fácilmente a los cambios si cuentan con la información suficiente. En resumen: Las ventajas de tener interfaces privadas y públicas en los objetos son las siguientes: 1. Los usuarios comunes acceden al objeto a través de las interfaces públicas exclusivamente, lo que protege al objeto de que sea dañado o afectado por un mal uso 2. En caso de que haya modificaciones en las partes internas del objeto, estas modificaciones sólo afectan la interface privada, por lo que no afectan a los usuarios comunes de los objetos De manera similar al ejemplo mostrado, en la programación orientada a objetos, los objetos poseen una interface pública, la cual puede ser accedida por cualquier otro objeto y una interface privada, la cual sólo puede ser accedida dentro del mismo objeto. En realidad, el manejo de interfaces públicas y privadas en la programación orientada a objetos es más complicado que lo presentado hasta ahora. Sin embargo, la idea en general es muy similar y también las razones por las que es conveniente “esconder” información. información. A continuación presentaremos los mismos conceptos pero los pondremos en práctica en el ejemplo de la nómina, el cual hemos utilizado hasta ahora en este libro.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
58
La figura 3.15 muestra la clase Trabajador de acuerdo a nuestra última definición. Es decir, incorpora el uso de sobrecarga de constructores.
Figura 3.15 Código de la clase Trabajador Cómo puede verse en el código de la figura 3.15, las líneas 4, 5, 6 y 7 declaran los atributos de los objetos de la clase Trabajador como públicos. Esto es por la palabra public que aparece a la izquierda de la declaración. Esto significa que se puede acceder a estos atributos desde cualquier clase que esté localizada en cualquier paquete. La figura 3.16 muestra la clase ClaseMala que asigna y lee valores de los atributos de un objeto de la clase Trabajador.
Figura 3.16 Código de la clase ClaseMala que asigna datos incorrectos a un Trabajador El resultado de la ejecución del código de la clase ClaseMala es el siguiente: El pago de Juan X es: -450
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
59
El resultado anterior, el cual es a todas luces inaceptable pues no puede haber un pago negativo, (excepto si la empresa le cobra al trabajador en lugar de pagarle). Empecemos por revisar el código de la figura 3.16. La línea 1 declara que el archivo está en el paquete etc. La línea 2 importa la clase Trabajador lo cual es necesario para que pueda ser utilizada por la clase ClaseMala. Las líneas 4, 5 y 6 son comentarios del programa. Es decir, una explicación del código. Los comentarios en Java se escriben empezando con los caracteres /* y terminando con */. Todo lo que esté entre estos dos pares de caracteres es ignorado por el compilador y su única utilidad es agregar legibilidad al programa. Otra forma de escribir comentarios es utilizando los caracteres // como se muestra en la línea 10. La diferencia entre estos dos tipos de comentarios es que este último tipo se usa comentarios de una sola línea. El error de este código está en la línea 10 pues se asigna un número negativo a las horas trabajadas. (t.horasTrabajadas=-3;). Obviamente, esto es incorrecto pues imposible que alguien trabaje un número negativo de horas. La pregunta es: ¿Cómo podríamos prevenir que se asigne un valor inaceptable a un atributo de un objeto? Otra pregunta más interesante aún ¿Quién debe ser el encargado de checar que los valores de los atributos sean válidos? La primera pregunta tiene muchas respuestas. ¿Puede el lector dar algunas soluciones? Por ejemplo, se podría checar que no se tengan números negativos en el momento de calcular el pago, o sea en el método calculaPago() de la clase Trabajador. Con respecto a la segunda pregunta: ¿Quién debe ser el encargado de checar que los atributos de los objetos sean válidos? La respuesta a esta pregunta es mucho más obvia: ¡El mismo objeto! Dado que el programador de la clase Trabajador es quien conoce a fondo a la clase (¡pues es quien la programó!) él es el más indicado para escribir el código que cheque la validez de los valores de los atributos. Esto está muy bien, bien, pero… ¿Cómo se puede prevenir que cualquier usuario (o clase) pueda asignar valores a los atributos de los objetos? ¡Haciéndolos privados! Si los atributos de los objetos son privados, no existe forma de que sus valores puedan ser alterados desde un código escrito en otra clase. Ésta es una forma de protección para que los valores de los atributos sólo puedan ser modificados (o incluso consultados) dentro de la misma clase. La figura 3.17 muestra el código de la clase Trabajador con la modificación de que ahora todos los atributos de los objetos de esta clase son privados.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
60
Figura 3.17 Código de la clase Trabajador con sus atributos privados Como puede observarse, ahora todos los atributos (tanto el de la clase como los de los objetos) objetos) son privados. Esto significa que sólo podrán ser “vistos” (consultados y modificados) dentro de la misma clase. Sin embargo, al compilar el código de la clase ClaseMala de la figura 3.16 obtenemos los siguientes errores: etc\ClaseMala.java:10: horasTrabajadas has private access in nomina.Trabajador t.horasTrabajadas = -3; // !El error esta aqui! ^ etc\ClaseMala.java:12: nombre has private access in nomina.Trabajador System.out.println("El pago de "+ t.nombre +" es: "+pago); ^ 2 errors
Los dos errores que marca el compilador compilador (líneas 10 y 12) tienen que ver precisamente precisamente con el hecho de que estamos estam os tratando de acceder a atributos privados desde “afuera” de la clase. Pues estos son atributos privados de la clase Trabajador y los estamos tratando de acceder desde la clase ClaseMala. Hemos corregido un problema pero pero ahora tenemos uno peor… ¡Ni siquiera podemos compilar el programa sin errores! ¿Qué hacer? La respuesta en la próxima sección. Setters y Getters
En la sección anterior, hemos visto que es conveniente tener atributos privados para que sólo pueda tener acceso a ellos el código escrito en la misma clase a la cual pertenecen dichos atributos. Sin embargo, a veces puede ser necesario este acceso “externo” a los atributos de los Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
61
objetos. La solución consiste en lo siguiente. Escribir métodos que tengan permisos públicos y que permitan acceder “desde afuera” a los atributos privados de los objetos. Estos métodos se escriben adentro de la clase y por lo tanto tienen acceso a los atributos privados pero al ser métodos públicos pueden ser llamados desde el exterior de la clase. El nombre que reciben estos métodos es “setters” y “getters”. Un método “setter” “ setter” se usa para asignar un valor a un atributo de un objeto y un método “getter” se utiliza para conseguir el valor de un atributo de un objeto. La figura 3.19 ilustra la idea de los setters y los getters a través de una analogía. La bola de boliche, es impenetrable. Sólo tiene tres agujeros los cuales permiten tomar la pelota para lanzarla.
Figura 3.18 Un objeto con atributos privados es como una bola de boliche Los getters de la figura 3.18 son los agujeros superiores y los setters (aunque en este caso es él setter porque sólo es uno) es el agujero inferior. Observamos por la dirección de las flechas que los setters permiten introducir algo (asignar un valor) al objeto mientras que los getters permiten sacar u obtener algo del objeto. Las figuras 3.19 y 3.20 muestran la forma de escribir un getter y un setter para el atributo horasTrabajadas. De la figura 3.19 en la línea 64 observamos que el método getter para el atributo horasTrabajadas es público, del tipo int (porque el atributo atributo es entero) y no recibe ningún ningún valor como parámetro. Por otro lado, en la figura 3.20 observamos que el setter debe ser público, void y si debe recibir un valor. Este valor es el que se asignará al atributo.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
62
Figura 3.19 El método getter del atributo horasTrabajadas Observamos también que en la línea 69 de la figura 3.20, se asigna el valor parámetro de entrada (horasTrabajadas) al atributo horasTrabajadas. Cómo el parámetro y el atributo tienen el mismo nombre, usamos la palabra this para referirnos al atributo.
Figura 3.20 El método setter del atributo horasTrabajadas La figura 3.21 muestra el código completo de la clase Trabajador en el cual hemos implementado todos los setters y getters para todos los atributos. En el caso del atributo estático (o sea, de la clase) numTrabajadores, no implementamos el setter pues no sería conveniente que cualquier clase externa pudiera modificar el valor de este atributo. Recordemos que sólo debe modificarse si se crea un nuevo objeto de la clase Trabajador como puede verse en el constructor de la clase (línea 14 de la figura 3.21). Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
63
Figura 3.21 Código de la clase Trabajador con atributos privados y métodos setters y getters La figura 3.22 muestra el código de la clase ClaseMala con las modificaciones necesarias para que pueda compilarse junto con la clase Trabajador de la figura 3.21.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
64
Figura 3.22 Código de la clase ClaseMala Como puede observarse en la línea 10 de la figura 3.22, 3.22, se utiliza un “setter “s etter”” para asignar el valor de -3 al atributo horasTrabajadas del objeto t. De esta forma, podemos podemos asignar un valor a un atributo privado desde otra clase. Aunque horasTrabajadas es privado, el método setter setHorasTrabajadas() es público y si puede ser llamado desde “afuera” de la clase Trabajador. De manera similar, en la línea 12 se utiliza el método getter getNombre(), del objeto t el cual se utiliza para conseguir el nombre del trabajador y mostrarlo en la salida del programa. El programa de la figura 3.22 compila y ejecuta sin errores produciendo la salida: El pago de Juan X es: -450
¡El mismo resultado inválido! Todos los cambios que hemos hecho y… ¡Seguimos teniendo el mismo resultado! El problema radica en que el método setHorasTrabajadas() no realiza ninguna validación sobre los datos que se envían. Es decir, asigna los valores a los atributos tal cual son enviados. Dicho de otra manera, aún podemos asignar un valor inválido al atributo horasTrabajadas de la clase Trabajador. Una manera de “blindar” al objeto para que no pueda tener valores inválidos es realizar una validación antes de asignar el valor al atributo del objeto en el método setter. La figura 3.23 muestra el código de la clase Trabajador con la validación correspondiente en el método setHorasTrabajadas de tal forma que acepte cualquier valor entre 0 y 168. Es decir suponemos que nadie puede trabajar menos de 0 horas ni más de 168 (24*7) en una semana. Después de modificar el código de la clase Trabajador de acuerdo a la figura 3.23, al ejecutar de nuevo el código de la figura 3.22 produce la salida: El pago de Juan X es: 0
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
65
Lo cual es aceptable (aunque posiblemente erróneo) pero la decisión que se tomó es asignar un valor default que en este caso es cero a cualquier valor que se salga del rango como lo hace la línea 42 de la figura 3.23.
Figura 3.23 Código de la clase Trabajador cuyo método setHorasTrabajadas() checa que el valor del atributo horasTrabajadas sea válido
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
66
De manera similar, podríamos modificar los métodos setters para los demás atributos de la clase Trabajador de tal forma que se validen los valores de los atributos antes de ser asignados. Se deja al lector como ejercicio. Resumen del Capítulo.
En este capítulo vimos algunos conceptos básicos de la programación orientada a objetos como son: los métodos y atributos de clase, los cuales a diferencia de los atributos de instancia, pertenecen a la clase y no al objeto. Éstos son necesarios cuando deseamos almacenar información general sobre la clase a la cual pertenecen un grupo de objetos. Otro concepto importante mencionado en este capítulo es el de constructor de un objeto. Un constructor es algo parecido a un método que se ejecuta al crear un objeto. Normalmente se usa para asignar valores a las variables de instancia o realizar procesos iniciales en los objetos. En este capítulo también vimos el uso de la palabra reservada “this” la cual se usa para hacer una autoreferencia al objeto o también para llamar a un constructor dentro de otro constructor del mismo objeto. Otros conceptos importantes vistos en este capítulo son: sobrecarga de constructores, arreglos, encapsulamiento a través del uso de “setters” y “getters” y permisos de acceso a miembros de los objetos. Ejercicios y preguntas.
1. Considere la clase BalónFutbol, la cual describe las características de un balón de futbol (soccer). De la siguiente lista de atributos, escriba una I si considera que el atributo debe ser considerado como un atributo de instancia o una C si el atributo debe ser considerado como de clase. a. pesoTotal ___ f. numeroDebalones ___
b. color ___ g. material ___
c. diámetro ___ d. fabricante ___ h. nombreDeporte ___
2. Considere el siguiente código:
a. ¿Cuál es la salida del código? b. ¿Por qué la asignación de la línea 9 no afecta la salida producida por la línea 12? Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
67
c. ¿Por qué no es necesario hacer referencia al objeto x en la línea 6 como se hace en la línea 7? d. ¿Cuántas localidades de memoria se utilizan para almacenar el contenido de TODOS los atributos que se utilizan en el código? 3. Escriba la clase JugadorDefutbol con los atributos nombre, edad y posición de tal forma que el siguiente código pueda compilar y al ejecutarse produzca la salida mostrada:
La salida debe ser: Jugadores: Edson,50,delantero Javier,20,desconocida
4. Escriba el código de la clase Alumno con los atributos de instancia, de clase y métodos setters y getters necesarios para que el siguiente código corra y produzca la salida mostrada.
La salida debe ser: Nombre: Juan, Edad: 20, Nivel: Licenciatura Nombre: Ernesto, Edad: 13, Nivel: Secundaria Número de Alumnos: 2
5. Modifique el código de la figura 3.13 de tal forma que se muestre al final el nombre del trabajador que recibirá el pago mayor. Sugerencia: use un ciclo “for” para recorrer todo el arreglo y comparar los valores. La salida salida deberá ser la siguiente:
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
68
6. Modifique el código de la figura 3.23 de tal forma que no sea posible asignar un número negativo al atributo salarioPorHora y además, el máximo valor que pueda asignarse sea de 150 pesos. Si se intenta asignar asignar un valor mayor a 150 pesos, entonces el “setter” deberá asignar deberá asignar 150 pesos. Nota: deberá modificar el método setSalarioPorHora(). Pruebe su código con el código que escribió para resolver el problema anterior. Describa y explique los cambios en la salida.
7. Suponga que por disposición oficial, a todos los trabajadores debe otorgárseles un bono de 50 pesos. ¿Cómo afectaría esta modificación a la interface pública de la clase Trabajador? ¿Se tendrían que realizar cambios al programa de la figura 3.13 para que produzca el resultado deseado? 8. El siguiente código contiene errores. Corríjalos y explique en qué consiste el error y cómo lo corrigió.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
69
9. Resuelva el siguiente problema utilizando arreglos. Se tiene una lista de 20 personas con los siguientes datos: Nombre, Edad y Sexo. Se desea encontrar la siguiente información: a) El promedio de edad de los hombres b) El promedio de edad de las mujeres c) El nombre de la mujer con mayor edad d) El nombre del hombre más joven e) El número de mujeres mayores al promedio de los hombres f) El número de hombres mayores al promedio de las mujeres 10. Construya la clase Estadisticas de acuerdo al siguiente diagrama de clases:
El método mayor encuentra el mayor de los elementos del arreglo, el método menor encuentra el menor, el método promedio encuentra el promedio de los elementos y el método suma regresa la suma de todos los elementos del arreglo. Note que todos los métodos son estáticos. Eso se puede observar en el diagrama porque están subrayados. Pruebe su programa con el siguiente código (colocar este archivo en el mismo directorio del archivo Estadisticas.java): Estadisticas.java):
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
70
public class PruebaEstadisticas { public static void main(String[] args) { int[] numeros = {1,2,3,4,10,6,7 {1,2,3,4,10,6,7,8,9,5}; ,8,9,5}; int mayor = Estadisticas.mayor(numeros); System.out.println("El System.out.prin tln("El mayor es: "+mayor); int menor = Estadisticas.menor(numeros); System.out.println("El System.out.prin tln("El menor es:"+ menor); float promedio = Estadisticas.promedio(numeros); Estadisticas.promedio(numeros); System.out.println("El System.out.prin tln("El promedio es:"+promedio); int suma=Estadistica suma=Estadisticas.suma(numeros); s.suma(numeros); System.out.println("La System.out.prin tln("La suma es:"+suma); }
} La salida del programa deberá ser: El mayor es: 10 El menor es:1 El promedio es:5.5 La suma es:55
11. Se les llama “arreglos multidimensionales” o “matrices” a los arreglos en los cuales cada elemento es a su vez otro arreglo. Se pueden representar gráficamente como tablas o cubos para dos o tres dimensiones respectivamente. Por ejemplo: (0,0)
(0,1)
(0,2)
(0,3)
(0,4)
(1,0)
(1,1)
(1,2)
(1,3)
(1,4)
(2,0)
(2,1)
(2,2)
(2,3)
(2,4)
(3,0)
(3,1)
(3,2)
(3,3)
(3,4)
(4,0)
(4,1)
(4,2)
(4,3)
(4,4)
Índices de la matriz
En Java, las matrices se declaran en forma similar a los arreglos, solo que en lugar de usar un par de corchetes se usan varios pares dependiendo del número de dimensiones de la matriz. Así, una matriz de números enteros como la del ejemplo, se declararía así:
int[][] matriz = new int[5][5];
El siguiente código, declara una matriz de 10 por 10 y le asigna números aleatorios entre 0 y 99:
public class Matrices { public static void main(String[] args) {
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
71
int[][] matriz = new int[10][10]; for (int i=0;i<10;i++){ for(int j=0;j<10;j++){ matriz[i][j]=(int)(Math.random()*100); } } // Escriba el código que muestre la matriz en pantalla // Escriba el código que encuentre el mayor de cada hilera } }
Substituya los comentarios por código que muestre la matriz y posteriormente que encuentre el mayor de cada hilera. Su salida deberá ser como esta: Matriz de enteros: 19 30 91 51 41 73 42 29 11 31 87 81 17 92 49 70 33 45 76 53 74 2 52 98 15 11 62 5 84 65 88 70 40 84 27 71 75 29 52 23
70 47 28 71 57 55 8 56 57 7
43 71 46 28 58 63 18 25 70 40
38 30 70 39 60 27 65 38 10 33
78 13 15 51 20 89 68 74 41 35
7 59 91 93 77 11 47 15 43 17
44 90 68 21 81 16 16 69 33 44 58
Mayores de cada 19 30 91 41 73 42 11 31 87 17 92 49 33 45 76 74 2 52
hilera: 51 70 29 47 81 28 70 71 53 57 98 55
43 71 46 28 58 63
38 30 70 39 60 27
78 13 15 51 20 89
7 59 91 93 77 11
44 90 68 21 81 16
MAYOR:91 MAYOR:91 MAYOR:90 MAYOR:91 MAYOR:93 MAYOR:81 MAYOR:98
15 84 40 75
5 70 71 23
18 25 70 40
65 38 10 33
68 74 41 35
47 15 43 17
69 33 44 58
MAYOR:69 MAYOR:88 MAYOR:84 MAYOR:75 MAYOR:75
11 65 84 29
62 88 27 52
8 56 57 7
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
72
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
73
4. Herencia y Polimorfismo En este capítulo trataremos una de las principales características de la programación orientada a objetos que es la Herencia y un concepto muy ligado a la herencia que es el Polimorfismo. Veremos cómo estos dos conceptos son muy útiles pero también agregan complejidad a la programación. Una de las implicaciones del uso de herencia es que tenemos más opciones para otorgar permisos de acceso a los miembros de la clase. En el capítulo anterior, vimos que podemos otorgar permisos públicos y privados. En este capítulo veremos que podemos otorgar permisos de paquete y protegidos. Otros conceptos relacionados con la herencia son: clases abstractas, interfaces, métodos abstractos, clases finales clases finales, y por último, veremos el concepto de retorno covariante. La figura 4.1 muestra m uestra la relación de la herencia con los otros o tros conceptos antes mencionados.
Figura 4.1 La Herencia es el eje central de una serie de conceptos importantes de POO
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
74
Concepto de herencia.
Como hemos insistido varias veces en este libro, la Programación Orientada a Objetos intenta representar el mundo real en la computadora. La herencia en POO es un mecanismo que intenta representar el concepto de especialización que existe en el mundo real. En el mundo real, existen conceptos que son más generales que otros (y por consiguiente, unos son más específicos que otros). Por ejemplo, un mueble es un concepto más general que una silla. Podemos decir que todas las sillas son muebles pero no todos los muebles son sillas. Una cama, por ejemplo es un mueble pero no es una silla. Entonces… ¿Qué hace que un concepto sea más general (o específico) que otro? La respuesta a la pregunta es que un concepto es más específico que otro si contiene mayor información que el otro (o sea el concepto específico tiene más información que el general). El concepto de mueble es el siguiente (según la real academia española): “Cada uno de los enseres movibles que sirven para los usos necesarios o necesarios o para decorar casas, oficinas y todo género de locales” Esta definición, no contiene información sobre el uso principal del mueble, ni su construcción, sin embargo, en las definiciones de silla y cama encontraremos mayor información. Por ejemplo que la cama está hecha para que las personas se acuesten en ella o que la silla se compone de asiento y respaldo. Entonces, un concepto más general tiene menos información pero abarca más objetos, mientras que un concepto más específico tiene más información pero abarca menos objetos. La figura 4.2 muestra la relación entre mueble, silla y cama.
Figura 4.2 Relación entre los conceptos de Mueble, Cama y Silla
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
75
La herencia en POO nos permite representar el concepto de especialización (o generalización) según lo vamos de arriba hacia abajo o de abajo hacia arriba. Podemos definir clases que heredan de otras (o sea, son más específicas). Por ejemplo, la clase Mueble es más general general que la clase Silla o que la clase Cama. La forma de indicar en Java que una clase es más específica (o sea, que hereda de) otra es utilizando la palabra extends en la declaración de la clase. La figura 4.3 muestra al código en Java que define estas tres clases y la relación entre ellas.
Figura 4.3 Definición de las clases Mueble, Silla y Cama Como puede verse en la figura 4.3, la línea 4 define la clase Silla como una extensión o especialización de la clase Mueble. De manera similar, la línea 7 define la clase Cama como una extensión de la clase Mueble. Hemos visto que el concepto de herencia en POO se usa para representar el concepto de especialización del mundo real. Sin embargo, no hemos visto aún cuál es su utilidad. Es decir, ¿Por qué es necesario o para qué queremos representar este concepto en la computadora? La siguiente sección intenta responder a estas preguntas. La Herencia como mecanismo de reúso de código.
Una de las principales razones para el uso de herencia en POO es el reúso de código. ¿Cómo funciona? Muy simple. Cuando especializamos (extendemos) una clase como en el ejemplo de la figura 4.3, las clases más específicas, las cuáles de ahora en adelante llamaremos subclases, heredan (por eso se le llama herencia a esta característica de la POO) todos los métodos y atributos de la clase más general, a la cual, de ahora en adelante llamaremos Superclase. En el ejemplo de la figura 4.4, las clases Silla y Cama heredan todos los métodos y atributos (note que no son privados) de la clase Mueble. Los atributos y métodos privados no se heredan aunque como veremos más adelante aún es posible usarlos con el código adecuado. Como pude verse en la figura 4.4, en la línea 16 se asigna in valor de 3 al atributo peso del objeto s de la clase Silla. Pero… ¡Ese atributo no fue definido en la clase silla! La razón por la cual
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
76
es posible utilizarlo es porque Silla es una subclase de la clase Mueble y por lo tanto hereda todos los atributos y métodos m étodos (no privados) de la clase Mueble. De manera similar, en la línea 18 se llama al método mueve() del objeto s aunque este método no haya sido definido en la clase Silla. Podemos concluir entonces que la clase Silla tiene todos los atributos y métodos definidos en la propia la clase, más todos los atributos y métodos de su superclase (en este caso, la clase Mueble). ¿Qué pasa si una subclase tiene una superclase que es a su vez es subclase de otra? Pues que todos atributos y métodos de las dos clases superiores son heredados a la subclase más específica. La figura 4.5 muestra esta idea.
Figura 4.4 Definición de las clases Mueble, Silla y Cama con métodos y atributos En la herencia no tenemos límites en cuanto al número de niveles que puede haber entre clases y subclases. Las subclases heredarán los métodos y atributos de las superclases y a su vez pasarán estos métodos y atributos a sus subclases y así sucesivamente. Como es obvio suponer, la última clase, es decir la más específica, contiene la mayor cantidad de información pues en cada nivel de la herencia se agrega nueva información, o sea, más atributos y/o métodos.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
77
La figura 4.5 muestra como las subclases heredan de las superclases y estas a su vez heredan de sus superclases y así sucesivamente. La clase Mueble tiene la característica: “movible”, la clase cla se Silla hereda esta característica y agrega otra más: “sirve para sentarse”. Finalmente, la clase SillaPlegable hereda características de todas sus clases superiores: “movible”, “sirve para sentarse” y agrega otra más: “Se puede plegar”. De esta forma, no es necesario redefinir características (o sea, métodos y atributos en las subclases siempre y cuando se hayan definido en las superclases. Mientras más específica sea una clase, tendrá mayor cantidad de información. Una observación: un programador no debe extender clases, a menos que realmente sea necesario, es decir, a menos que se agregue información a la superclase.
Figura 4.5 Herencia entre varias clases en la misma línea jerárquica Ahora, la pregunta es… ¿Dónde está el reúso de código? Pues precisamente en el hecho de que no es necesario volver a escribir métodos y atributos para las subclases. Una vez que un método o atributo es definido en una clase, todas sus subclases heredarán esos métodos y atributos. El reúso de código obviamente es muy importante pues permite ahorrar grandes cantidades de esfuerzo, tiempo y dinero. Pero… ¿Por qué no hacer desde un principio las clases lo suficientemente específicas?... Es decir… ¿Por qué no de finir desde el principio las clases con toda la información para que no sea necesaria la creación de clases más específicas? Bueno… la respuesta es que la programación no es una actividad de una sola persona. Cuando un programador escribe un programa, se basa en el trabajo de otros programadores. Sería muy difícil escribir un programa “desde cero”. En particular, los programadores que escriben programas
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
78
orientados a objetos extienden clases escritas por otros programadores y les agregan características específicas específicas al problema que desean resolver. La figura 4.7 ilustra esta idea. La figura 4.6 ilustra el hecho de que la programación es una actividad colectiva en la cual los programadores se basan en el trabajo de otros programadores. En la figura 4.6 observamos que el programador de cierta compañía utiliza (extiende) una clase que le permite mostrar una ventana. La superclase (JFrame) fue creada por otro programador, el cual no conoce todos los detalles del tipo de ventana que será finalmente utilizada. Sin embargo, conoce algunos atributos y métodos que toda ventana debe tener. Por ejemplo, que la ventana debe tener métodos para dibujarse, abrir, cerrar, cambiar de tamaño, etc. Así mismo, debe tener atributos como color de fondo, tamaño, etc.
Figura 4.6 Programadores extendiendo clases de otros programadores Corresponde al programador de la compañía extender la ventana general agregando los detalles particulares de la compañía. Por ejemplo, la información que se mostrará en la ventana, el logotipo de la empresa, etc. La herencia es, entonces, un mecanismo muy importante para el reúso de código con lo cual los programadores ahorran una gran cantidad de esfuerzo al utilizar código previamente escrito por otros programadores (o por ellos mismos). Permiso de Acceso Protegido
Como se mencionó anteriormente, el permiso de acceso público permite que cualquier clase desde cualquier paquete pueda tener acceso a los miembros públicos de una clase, mientras que el método de acceso privado prohíbe cualquier acceso desde cualquier clase. Entonces una pregunta que podría surgir es… ¿No hay algo en medio? Es decir… ¿Existe alguna forma de acceso que no sea tan restrictiva como como la privada pero tampoco tan relajada como la pública? pública? La respuesta es sí. De hecho, existen otras dos formas de acceso de las que hablaremos un poco en ésta y en la siguiente sección.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
79
Supongamos que deseamos definir una clase cuyas subclases puedan heredar sus características (o sea, sus métodos y atributos). Pero no deseamos ponerlos públicos porque no queremos que cualquier clase tenga acceso a ellos. ¿Qué podemos hacer? Respuesta: Utilizar el permiso de acceso protegido (protected). Este tipo de permiso otorga a cualquier subclase acceso a los métodos y atributos de la superclase, aunque ambas estén en diferentes paquetes. La figura 4.7 muestra un ejemplo de código que utiliza este tipo t ipo de permiso de acceso.
Figura 4.7 La clase Deportista con atributos protegidos La clase Deportista de la figura 4.7 fue diseñada de tal forma que los atributos nombre y edad declarados en las líneas 3 y 4 sólo pueden ser accedidos por subclases de esta clase. De esta forma, ninguna clase que no sea subclase de la clase Deportista (aunque esté en el mismo paquete) puede tener acceso a estos atributos. La palabra “ protected” a la izquierda del tipo del atributo establece este premiso de acceso. Las figuras 4.8 y 4.9 muestran las clases Futbolista y Nadador. Ambas clases extienden la clase Deportista y heredan sus atributos (nombre y edad) aunque cada una de las tres clases está en diferente paquete.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
80
Figura 4.8 Clase Futbolista que extiende la clase Deportista
Figura 4.9 Clase Nadador que extiende la clase Deportista Como puede observarse de las figuras 4.8 y 4.9, ambas clases (Futbolista y Nadador) extienden de Deportista y por lo tanto heredan los atributos (nombre y edad) de esta clase. Sin embargo, cada clase añade sus propios atributos. Por ejemplo, la clase Futbolista añade dos atributos: numeroCamiseta y posición. Es importante señalar que estos atributos ¡Sólo tienen sentido para un futbolista! Un nadador no utiliza número de camiseta (¡Ni siquiera utiliza Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
81
camiseta!) ni posición. De manera similar, los atributos especialidad y mejorTiempo sólo tienen sentido en objetos de la clase Nadador. Algo que el lector puede observar es que los atributos de las subclases son privados ( private). Esto significa que no deseamos que las subclases de estas clases los hereden. La figura 4.10 muestra la relación entre las clases Deportista, Futbolista y Nadador utilizando la notación UML Como puede verse en la figura 4.10, la herencia en POO se representa mediante una flecha que va de la subclase a la superclase. Esto significa que la clase de la cual sale la flecha extiende la clase a la que llega la flecha. Por lo tanto, todos los atributos y métodos (que no sean privados) de la superclase pertenecen también a la subclase. En la figura 4.10 observamos también que se utilizan ciertos símbolos para representar el tipo de permiso de acceso de los atributos y los métodos de los objetos. Aunque tanto los libros como el software de diagramación a menudo no coinciden en dichos símbolos, la especificación de la versión 2.0 de UML establece que los símbolos para los tipos de permiso: público, privado, protegido y de paquete (este lo veremos en la siguiente sección) son respectivamente: “+”, ““ -“, “#” y “~”. La figura 4.10 no es del todo correcta con respecto al código presentado para las clases Deportista, Futbolista y Nadador pues no representa el hecho de que estas clases están en distintos paquetes. La figura 4.11 muestra las clases con sus respectivos paquetes.
Figura 4.10 Diagrama de Clases UML que muestra las clases Deportista, Futbolista y Nadador
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
82
Figura 4.11 Diagrama de Clases UML que muestra las clases Deportista, Futbolista y Nadador con sus respectivos paquetes
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
83
Finalmente, la figura 4.12 muestra la clase Main que crea dos deportistas y les asigna valores a sus atributos heredados y atributos propios. También hace uso de métodos heredados y métodos propios.
Figura 4.12 Uso de las clases Futbolista y Nadador que heredan de la clase Deportista. Podemos ver en las líneas 3 y 4 que la clase Main debe importar las clases Futbolista y Nadador porque crea objetos de estas clases. Sin embargo, no es necesario importar la clase Deportista, porque esta clase es importada por cada una de las subclases (además de que Deportista y Main están en el mismo paquete).
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
84
Permiso de acceso de Paquete.
Aunque el permiso de acceso de “paquete” (también llamado “default” en Java) no es necesariamente un tema relacionado con la herencia, lo hemos incluido en este capítulo para completar los diferentes tipos de permisos de acceso. Hemos visto que el tipo de acceso público permite que los miembros de una clase puedan ser accedidos desde cualquier clase en cualquier paquete. Por otro lado, miembros privados sólo pueden ser accedidos desde d esde la misma clase. Finalmente, vimos que los miembros “protegidos” sólo pueden ser accedidos por subclases de la clase a la que pertenecen. Existe aún otro tipo de permiso de acceso que en Java es el default. Es decir, si no se escribe el tipo de acceso a la izquierda del método o atributo, se asume este tipo de acceso. Se le llama acceso de “paquete” porque permite que las clases que estén en el mismo paquete tengan acceso a los miembros declarados con este tipo de permiso. El código de la figura 4.13 compila y se ejecuta correctamente pues la clase Pago está en el mismo paquete que la clase Trabajador. De hecho, está en el mismo archivo. Todas las clases que estén en el mismo archivo, están también en el mismo paquete pues un archivo sólo puede pertenecer a un paquete.
Figura 4.13 Código de las clases Trabajador y Pago Si colocamos la clase Pago en otro paquete como lo muestra la figura 4.14, el compilador nos marcaría un error pues estamos tratando de acceder a los atributos con permiso de paquete desde una clase situada en otro paquete. El error producido por el compilador se muestra en la misma figura 4.14. Es interesante observar que a pesar de que si tenemos acceso a la clase Trabajador, no tenemos acceso a los atributos de los objetos de esa clase. Por esta razón, el error que marca el compilador es en la línea 7 y no en la línea 6. Es decir, podemos crear objetos de la clase Trabajador pero no podemos acceder a sus atributos. Por supuesto, ya sabemos que podríamos tener acceso si escribimos un getter y un setter con permiso público, pero no es posible acceder a los atributos de manera directa. Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
85
Figura 4.14 Código de la clase Pago tratando de acceder a miembros con permiso de paquete de la clase Trabajador desde otro paquete Sobre-escritura de Métodos.
Regresemos al tema central de este capítulo, que es la herencia. En muchas ocasiones, es necesario que una subclase redefina (o sobre escriba) un método que fue definido en la superclase. Por ejemplo, supongamos que vamos a escribir código para un videojuego el cual utiliza varios personajes, armas, niveles, naves, etc. Después de determinar cuáles son las clases necesarias, Llegamos a la conclusión que necesitamos tener, entre muchas otras, una clase Arma y dos subclases: RifleLaser y LanzaFuego. La clase Arma debe tener un atributo carga que indique cuanta carga tiene (o sea, el porcentaje de carga de municiones o “ammo” como le llaman en los videojuegos) y un método dispara() el cual realiza el disparo según el tipo de arma. La figura 4.15 muestra el código de estas clases. La línea 5 de la figura 4.15 declara el método dispara() de la clase Arma. Como este método es público, es heredado por las subclases RifleLaser y LanzaFuego. Sin embargo, no es lo suficientemente explícito, pues deseamos que al disparar, cada arma indique como se está disparando. En el caso de el RifleLaser debe mencionar que se dispara un rayo laser y en el caso del LanzaFuego debe mencionar que se está lanzando fuego. Pero eso no es todo, al disparar, se disminuye la carga de manera diferente. El RifleLaser disminuye su carga 10% y el LanzaFuegos disminuye su carga 5%. Por esta razón, las clases RifleLaser y LanzaFuego también definen el método dispara(). Al hecho de que una subclase redefina un método método de la superclase, se le llama sobre-escritura de métodos. Lo anterior puede verse en las líneas 17-20 y 24-27 de la figura 4.15.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
86
El método dispara() de la clase Arma ha sido sobre-escrito en las subclases LanzaFuego y RifleLaser. ¿Qué repercusiones tiene sobre-escribir un método? La respuesta la encontraremos al compilar y ejecutar el código de la figura 4.16.
Figura 4.15 Código de las clases Arma, RifleLaser y LanzaFuego Al compilar y ejecutar el código de la figura 4.16, obtenemos el resultado esperado: ¡Arma Disparando con 100% de intensidad! ¡Disparando un rayo laser con 100 de intensidad! ¡Lanzando fuego con 100% de intensidad!
Las líneas 5, 6 y 7 de la figura 4.16 crean tres objetos las clases Arma, RifleLaser y LanzaFuego respectivamente. Al momento de llamar al método dispara() de cada uno de los objetos de las subclases subclases , lo cual se hace hace en las líneas 9 y 10, el método que se se ejecuta es el de de la subclase. Entonces, podemos concluir que una subclase hereda los métodos de las superclase, pero… Si los métodos son redefinidos (sobre-escritos) (sobre -escritos) en la subclase, entonces el método heredado se desecha y se utiliza el método definido en la subclase. ¿Qué pasaría si eliminamos la definición del método dispara() de la clase LanzaFuego? Dejamos ese experimento al lector. Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
87
Figura 4.16 Uso de las clases Arma, RifleLaser y LanzaFuego Clases Abstractas.
En muchas ocasiones, un programador desea crear una clase y conoce cuáles son los atributos y los métodos que deben tener los objetos de la clase. Sin embargo, desconoce la implementación de algunos o de todos los métodos. Para ilustrar este concepto, tomemos el mismo ejemplo de la sección anterior. Si analizamos la clase Arma, podemos llegar a la conclusión que no tiene mucho sentido crear objetos de esta clase. La razón razón es muy simple… ¿Para qué crear un objeto de la clase Arma si podemos crear un objeto más específico como un RifleLaser o un LanzaFuegos? Además, la carga se disminuye de manera diferente por cada disparo de cada tipo de arma (uno disminuye un 5% y el otro 10%). El método dispara() de la clase Arma no disminuye la carga porque no se puede saber si el arma es un RifleLaser o un LanzaFuegos. Conclusión: no es es conveniente crear objetos de la clase Arma. Por otro lado, si es conveniente crear la clase Arma porque tiene atributos (en esta caso, el atributo carga) que son heredados a sus subclases. Entonces, tenemos un dilema: Queremos crear una clase pero no queremos que se creen objetos a partir de esa clase y además queremos definir un método pero no queremos implementarlo implementarlo en la clase sino más bien que lo implementen las subclases . Lo que necesitamos es crear una clase abstracta. Una clase abstracta es aquella que puede tener métodos abstractos y de la cual no se pueden crear objetos. Un método abstracto es aquel en el cual sólo se define el encabezado, es decir el
nombre del método, el tipo de retorno, los permisos de acceso y los parámetros. No se define el cuerpo del método, es decir las instrucciones. La figura 4.17 muestra la clase Arma definida como abstracta.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
88
Figura 4.17 Definición de la clase abstracta Arma y sus subclases LanzaFuego y RifleLaser La línea 3 de la figura 4.17 define la clase Arma como abstracta. La línea 6 de la misma figura define el método dispara() como abstracto. Como puede verse, no se ha definido el cuerpo del método, o sea, las instrucciones que se ejecutarán cuando se llame al método. Si queremos ejecutar código de la figura 4.16 con esta modificación, nos producirá un error de compilación: C:\...\Main.java:5: armas.Arma is abstract; cannot be instantiated C:\...\Main.java:5: Arma a = new Arma(); 1 error
El error anterior ocurre porque en la línea 5 estamos tratando de crear un objeto de la clase Arma, la cual fue declarada como abstracta. Eliminando las líneas 5 y 8 de la figura 4.16, nos produce la salida esperada: ¡Disparando un rayo laser con 100 de intensidad! ¡Lanzando fuego con 100% de intensidad!
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
89
Debe quedar muy claro entonces que las clases abstractas no son instanciables. Es decir, no se pueden crear objetos de estas clases. Algo que es también muy importante señalar es que el programador que extiende una clase abstracta está obligado a implementar todos aquellos métodos abstractos que son heredados a las subclases (siempre y cuando estas no sean abstractas). En el ejemplo de la clase Arma, las clases Lanzafuego y RifleLaser deben implementar el método dispara(). Por ejemplo, si eliminamos las líneas 24-28 de la figura 4.17, el compilador marcará el siguiente error: armas.RifleLaser is not abstract and does not override abstract method dispara() in armas.Arma at armas.RifleLaser.(Arma. armas.RifleLaser.(Arma.java:23) java:23)
Lo que está indicando el compilador es que el método dispara() no ha sido implementado en la clase RifleLaser y debe ser implementado porque es un método abstracto que pertenece a la superclase Arma. Tomemos por favor un tiempo para pensar un poco sobre esta obligación que tienen las subclases (o más bien los programadores que las programan) de implementar los métodos abstractos de la superclases. Primero, hagamos una analogía: Recordemos que una clase es como el plano de una casa y el objeto es como la casa. Si le damos un plano de una casa incompleto a un constructor, obviamente ¡No podrá construir la casa! (Figura 4.18). Podemos ver a una clase abstracta como un plano incompleto pues los métodos abstractos no tienen cuerpo. O sea, le falta información pues no está especificado cómo implementar estos métodos. Por esta razón, el compilador marca error al momento de tratar de instanciar una clase abstracta o al momento de extender la clase abstracta sin implementar todos sus métodos abstractos. Por un momento, supongamos que una subclase no implementa un método abstracto de su superclase abstracta. Consideremos el código de la figura 4.19. Este código como ya lo habíamos mencionado, marca un error. Pero supongamos que pudiéramos compilar sin error. Como puede verse en la figura 4.19, el método dispara() de la clase RifleLaser no se implementó. La clase RifleLaser extiende de la clase Arma, y por lo tanto, hereda el método abstracto dispara(). Entonces ¿Qué pasará cuando se ejecute la línea 31? ¡El mundo como lo conocemos se acabaría! Bueno, quizás no sería tan grave el resultado, pero si habría un grave problema porque la computadora no “sabría” qué hacer al momento de ejecutar el método método dispara() del objeto r de la clase RifleLaser. Ahora, podemos preguntarnos… ¿Es posible extender una clase abstracta en otra clase abstracta? La respuesta es sí. Otra pregunta… ¿Es posible dejar métodos sin implementar (abstractos) en la subclase abstracta? La respuesta es otra vez sí. ¿Por qué? Recordemos que una clase abstracta no es instanciable. Cuando un programador declara que una clase es abstracta es como si hiciera la Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
90
promesa solemne: “Juro que jamás intentaré crear un objeto de esta clase”. clase”. Entonces el compilador no tiene ningún problema en dejar que queden métodos sin implementar porque nunca ocurrirá la tragedia de que se llame un método que no ha sido implementado. Una clase abstracta también puede tener métodos concretos (o sea, implementados). De hecho, una clase abstracta puede tener TODOS sus métodos implementados. Es un poco raro encontrar esta situación en la vida real, r eal, pero es posible y perfectamente válido.
Figura 4.18 Tratar de instanciar una clase abstracta es como tratar de construir una casa con un plano incompleto Si modificamos el código de la figura 4.19, específicamente en la línea 23, agregando la palabra abstract antes de la palabra class, el compilador ya no marcaría el error: armas.RifleLaser is not abstract and does not override abstract method dispara() in armas.Arma at armas.RifleLaser.(Arma. armas.RifleLaser.(Arma.java:23) java:23)
Esto es porque aunque estamos dejando sin implementar el método dispara() de la clase RifleLaser, estaríamos también declarando la clase RifleLaser como abstracta. Pero ahora marcaría otro error: armas.RifleLaser armas.RifleLase r is abstract; cannot be instantiated at armas.Main.main(M armas.Main.main(Main.java:31) ain.java:31)
Ahora obtenemos este error porque estamos tratando de instanciar la clase RifleLaser la cual fue declarada como abstracta. Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
91
Para aquellos que no están muy interesados en el porqué de las cosas, a continuación mostramos una serie de reglas que se deben de recordar al momento de escribir un programa que utiliza clases abstractas. Si en algún momento, se despierta este interés, favor de leer las secciones anteriores.
Figura 4.19 Extendiendo una clase abstracta sin implementar (sobre-escribir) su método abstracto Reglas para programar con clases abstractas: 1. Las clases abstractas no se pueden instanciar. 2. Las clases abstractas pueden tener métodos abstractos. Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
92
3. Las clases concretas (o sea las que no son abstractas) no pueden tener métodos abstractos. 4. (Esta es consecuencia de la anterior) las clases concretas que extienden clases abstractas deben implementar todos los métodos abstractos de las superclases. 5. Las clases abstractas pueden tener todos to dos sus métodos implementados.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
93
Interfaces.
Recordemos que el objetivo de la Programación Orientada a Objetos es modelar la realidad en la computadora. Existe otro concepto en la realidad que es muy útil y que los lenguajes de programación nos permiten de alguna o de otra forma modelarlo. Este concepto es la Herencia Múltiple. Poniéndolo de manera muy simple, la herencia múltiple consiste en el hecho de que una subclase tenga dos o más superclases. Por ejemplo, un teléfono inteligente (smartphone) es un dispositivo de comunicaciones y por lo tanto tiene ciertas funciones que permiten la comunicación entre personas como realizar realizar y recibir llamadas, llamadas, envío y recepción recepción de mensajes entre otros. Sin embargo, un smartphone también es un dispositivo de entretenimiento y por lo tanto, hereda también funciones de este tipo de dispositivos como son: reproducir música, reproducir videos, mostrar imágenes, etc. (Figura 4.20)
Figura 4.20 Ejemplo de un objeto cuya clase tiene dos superclases Podemos entonces decir que un Smartphone es una subclase de Teléfono pero pero también es una subclase de DispositivoEntretenimento. La figura 4.21 muestra el Diagrama de Clases de esta relación de herencia múltiple. En este ejemplo, nos referimos a un dispositivo de entretenimiento de video, como podría ser un reproductor de DVD o un videojuego conectado a una pantalla de televisión por supuesto. Por un lado, los teléfonos tienen el volumen del timbre como atributo y permiten realizar llamadas. Por otro lado, los dispositivos de entretenimiento (en este caso un reproductor de video) tienen como atributo la resolución, o sea las dimensiones en Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
94
pixeles con la que reproducen el video, y la función de reproducir el video. El smartphone tiene todos estos atributos y estas funciones y además tiene las propias del smartphone como son el tamaño en pixeles con las que toma las fotos y la función de enviar la ubicación o posición en donde se encuentra la persona a través de un sistema de posicionamiento global (GPS)
Figura 4.21 Diagrama de Clases del Ejemplo de Herencia Múltiple La figura 4.21 muestra que la clase Smartphone es subclase de Teléfono y de DispositivoEntretenimiento. Por lo tanto, hereda todos los atributos y métodos de ambas clases. Podemos estar tentados a escribir el siguiente código:
class Smartphone extends Teléfono, DispositivoEntretenimiento { …
}
Sin embargo, el código de arriba es erróneo en Java porque Java no permite la herencia múltiple. La razón por la cual no la permite tiene que ver con uno de los objetivos del lenguaje, que es su simplicidad. La herencia múltiple, a pesar de que es un concepto muy importante, también trae problemas inherentes a él, como por ejemplo, el problema del “Diamante “D iamante Mortal” Mortal” (Deadly (Deadly Diamond of Dead). Este problema sucede cuando dos o más superclases tienen atributos o métodos con el mismo nombre. En este caso, hay ambigüedad sobre cuál de los atributos o métodos se hereda. La figura 4.22 muestra el problema del diamante mortal con un ejemplo. La figura 4.22 muestra una situación en la cual una clase tiene dos subclases. La clase Trabajador contiene el método calculaPago(). Las clases Docente y Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
95
Administrativo, las cuales son subclases de Trabajador, sobre-escriben dicho método.
Finalmente, la clase AdministrativoDocente ¡Hereda el método calculaPago() de las dos superclases! El problema está en la ambigüedad sobre cuál de los métodos calculaPago() debe escogerse.
Figura 4.22 El problema del Diamante Mortal en la Herencia Múltiple Hemos establecido que Java no permite la Herencia Múltiple. Entonces ¿Cómo podemos modelar el hecho de que un objeto de cierta clase pueda heredar características de dos o más clases? A través del uso de interfaces. Una interface en Java es un tipo especial de clase abstracta en la cual todos sus métodos son abstractos. Existen algunas características adicionales de las interfaces, pero por lo pronto, tomaremos la definición anterior. Las interfaces no pueden tener métodos concretos. Es decir, cuando un programador crea una interface, solamente define cuales serán los métodos que deberán ser implementados por las clases que implementen la interface y no define el cuerpo, o sea, las instrucciones que tienen cada método. Regresemos al ejemplo del videojuego. Tenemos la clase Arma, la cual es abstracta porque sabemos que las armas deben disparar pero no podemos determinar a detalle cómo hacerlo, ya que cada subclase de esta clase abstracta dispara de manera diferente (ver figura 4.19). Las implementaciones del método dispara() se encuentran en cada una de las subclases RifleLaser y LanzaFuegos. Supongamos que hemos decidido que un objeto de la clase LanzaFuegos puede ser recargado si el personaje que lo usa encuentra un tanque de gas. Sin embargo, un RifleLaser no hay Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
96
forma de recargarlo, pues la batería atómica no es reemplazable. Además, el LanzaFuegos no es el único objeto recargable. Existen otros objetos recargables como son: Linterna y LanzaGranadas, entre otros. La figura 4.23 4 .23 muestra estos objetos y sus características.
Figura 4.23 Algunos Objetos de un Videojuego y sus Características Los objetos recargables tienen cierto comportamiento similar, o sea, se pueden recargar. Entonces podríamos crear la clase abstracta Recargable con su método recargar()para que todas las clases hereden este método. Sin embargo, los objetos de la clase LanzaFuego también son armas (de la clase Arma). Entonces… ¡Tenemos un caso de herencia múltiple! En Java, no se permite la herencia múltiple, es decir, no podemos tener la clase LanzaFuego como subclase de las clases Arma y Recargable. Sin embargo, podemos crear la clase Recargable como una interface y especificar que un LanzaFuego es una Arma pero también se comporta como un objeto Recargable. La figura 4.24 muestra cómo hacer esto en Java. La línea 18 de la figura 4.24 declara la clase LanzaFuego extiende la clase Arma e implementa la interface Recargable. Esto significa que los objetos de la clase LanzaFuego se comportarán como Armas pero también se comportarán como objetos Recargables. Como se indica en la figura 4.24, cuando una clase (concreta) extiende una clase abstracta y/o implementa una interface, debe de implementar todos los métodos abstractos heredados de ellas, de lo contrario el programa no compila. ¿Por qué? Porque Porque las clases concretas concretas son instanciables (es (es decir, se pueden crear objetos de estas clases) entonces, si no se implementaran todos los métodos abstractos… ¡Crearíamos objetos objetos incompletos! (ver figura 4.18).
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
97
Figura 4.24 Uso de Interfaces en Java
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
98
La línea 14 de la figura 4.24 declara la interface recargable. Las interfaces se declaran de manera muy similar que las clases, incluso también pueden extender otras interfaces. Sin embargo, la principal diferencia entre las clases y las interfaces es que TODOS los métodos de las interfaces deben ser abstractos. De hecho, aunque no se ponga la palabra abstract se asume que el método es abstracto (y público). Finalmente, observamos que la línea 48 de la figura 4.24 declara la clase Linterna. Esta declaración no contiene la palabra extends. Esto no significa que la clase Linterna no tenga ninguna superclase. Existe una clase que es superclase de todas las clases. Ésta clase se llama Object. Si en la declaración de una clase, se omite la palabra extends entonces se asume que esa clase extiende de la clase Object. Sin embargo, la línea 48 si contiene la palabra implements. Esto significa que la clase Linterna está implementando la interface Recargable. Por lo cual, debe implementar el método recargar(). Resumiendo lo anterior, en la Programación Orientada a Objetos, tenemos el concepto de herencia múltiple. La herencia múltiple se da cuando un objeto puede heredar características de varias clases. Por ejemplo, tenemos armas que también son objetos recargables. Sin embargo, Java no permite la herencia múltiple, la forma de representar este concepto es a través del uso de interfaces. Una interface es un tipo de clase abstracta en la cual TODOS los métodos son abstractos. Otro aspecto importante de la implementación del concepto de herencia en el lenguaje Java es que una clase sólo puede extender otra, pero puede implementar muchas interfaces. Tantas como sean necesarias. Por ejemplo, en el caso de que el LanzaFuego implementara también la interface Destructible (objetos que pueden ser destruidos). Entonces sólo agregaríamos esta nueva interface a la línea 18: class LanzaFuego extends Arma implements Recargable, Destructible { …
El lector podría entonces hacerse la pregunta… ¿Cómo se evita el problema del diamante diama nte mortal, si de cualquier forma se permite que se hereden métodos abstractos de varias interfaces? La respuesta es muy simple: Las interfaces no pueden tener implementados sus métodos. Entonces, aún en el caso de que una clase implementara varias interfaces con métodos con el mismo nombre… ¡Solo podría haber una implementación!... ¡Problema resuelto! (Ver Figura 4.25) En la figura 4.25 vemos un diagrama UML de clases que muestra dos interfaces: VehiculoMotorizado y ObjetoInflamable. Ambas interfaces tienen el método abstracto encender(). Obviamente, aunque el método se llama igual en las dos interfaces, tiene un significado diferente pues en el caso del vehículo se refiere a ponerlo en marcha mientras que en el caso del objeto inflamable se refiere a que comience a arder en llamas. En la misma figura, observamos la clase Automovil. Ésta clase implementa implementa ambas interfaces, por lo cual hereda hereda el método abstracto encender(). A pesar de esta ambigüedad, se puede escribir un programa que Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
99
represente estas relaciones sin ningún problema pues se tendrá que definir la implementación del método en la clase Automovil. Finalmente, vemos que en UML, se utilizan cursivas para representar clases y métodos abstractos. Las interfaces se representan agregando además de las letras cursivas, la leyenda: <> en la parte superior. La implementación de una interface se representa por medio de una flecha punteada de la clase a la interface.
Figura 4.25 Diagrama UML de clases que muestra la clase Automovil implementando dos interfaces con el mismo método abstracto. Utilizando las Clases del Videojuego
Regresemos al ejemplo del videojuego. Supongamos ahora un escenario en el cual el soldado, el cual es el personaje principal, comienza disparando todas sus armas y posteriormente entra a una estación de recarga en la cual puede recargar todas sus armas. La linterna inicialmente no tiene batería, pero también se recarga en la estación. Las figuras 4.26 y 4.27 muestran el código de las clases Soldado y Escenario.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
100
El código de las figuras 4.26 y 4.27 se encuentra en el mismo archivo de la figura 4.24. Por esta razón los números de línea continúan. Debido a que tanto el RifleLaser como el LanzaFuego y el LanzaGranada extienden la clase abstracta Arma, estamos seguros que los tres objetos tienen el método dispara(). De lo contrario, el programa no compilaría. Esto lo podemos ver en las líneas 77, 78 y 79 de la figura 4.26. Por otro lado, el método recargaTodo() que está implementado implementado en las líneas líneas 81 a 86 de la figura 4.26, manda llamar al método recargar()de los objetos que implementan la interface Recargable (LanzaFuego, LanzaGranada y Linterna ). Finalmente, el método muestraEstado() muestra en la consola de la computadora el nivel de carga de las armas y el nivel de batería de la linterna. Observe que el método getCarga() fue implementado en la clase Linterna pues dicho método no fue heredado de la clase Arma porque… porque… la ¡Linterna no es una arma! En otras palabras, Linterna no extiende Arma como puede verse en la línea 48 de la figura 4.24. 4.24 . Sólo implementa la interface Recargable. La figura 4.27 muestra la clase Escenario, la cual implementa el escenario descrito anteriormente. Primero, se crea el objeto de la clase Soldado (Figura 4.27, línea 100). Posteriormente, se crean los tres objetos correspondientes correspondientes a sus armas (líneas 102 a 104). La línea 105 crea la linterna. Las líneas 107 a 110 utilizan los setters de la clase Soldado para asignar los valores de los atributos. Finalmente, las líneas 112 a 115 llaman los métodos que disparan las armas, muestran el estado, recargan el equipo y vuelven a mostrar el estado. La salida de la ejecución del código de las figuras 4.24, 4.26 y 4.27 se muestra en la figura 4 .28.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
101
Figura 4.26 Clase Soldado que utiliza las clases LanzaFuego, LanzaGranada, RifleLaser y Linterna
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
102
Figura 4.27 Código de la clase Escenario la cual crea un Soldado con su equipo y lo pone en acción.
Figura 4.28 Salida de la Ejecución del código de la clase Escenario
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
103
Polimorfismo
Aunque existen varios tipos de Polimorfismo, en esta sección nos referiremos al “polimorfismo de subtipos” al que comúnmente se le conoce simplemente simplemente como como “polimorfismo”. “polimorfismo”. Antes de dar una definición del concepto, consideremos el siguiente ejemplo. Supongamos que queremos modelar el funcionamiento de un restaurante de comida internacional. Los clientes pueden encontrar comida mexicana, china e italiana italiana entre otras. Obviamente, se esperan clientes de varias nacionalidades y por lo tanto el restaurante debe estar preparado para atender a cada uno de ellos. Los meseros del restaurante hablan varios idiomas y sirven comida de acuerdo a la nacionalidad del cliente. Por ejemplo, a un mexicano le servirán tacos, a un chino arroz y aun italiano pizza. El modelado del problema lo haremos a través de las clases Mesero, Cliente, Chino, Mexicano e Italiano como se muestra en la figura 4.29.
Figura 4.29. Modelado del Restaurante Internacional La figura 4.29 muestra la clase Mesero, la cual tiene 3 métodos Los métodos son: atiende(Chino), atiende(Mexicano) y atiende(Italiano). Recordemos que cuando sobrecargamos un método, escribimos el mismo nombre del método pero con diferentes parámetros. En este caso, escribimos un método para cada diferente tipo de cliente que llegue al restaurante. También podemos observar que las clases Italiano, Mexicano y Chino son subclases de la clase abstracta Cliente. Finalmente, observamos que la clase Cliente y sus subclases tienen un constructor que les permite asignar inicialmente el nombre del cliente. Las figuras 4.30 a 4.35 muestran el código de las clases mostradas en la figura 4.29. La figura 4.36 muestra el resultado de la ejecución de la clase Main la cual crea un mesero, tres clientes y llama al método atiende() para cada uno de los clientes.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
104
Figura 4.30 Código de la clase Cliente Podemos observar en la figura 4.30, que se declara la clase abstracta Cliente en la línea 3 aunque no contiene métodos abstractos. En este caso, la razón por la que fue declarada abstracta no es que la clase tenga métodos abstractos. De hecho, tenemos toda la información de los clientes (atributos y métodos). La razón por la cual la creamos abstracta es porque no queremos crear objetos de la clase Cliente en el programa. Puede haber situaciones en la cuales el programador podría desear crear objetos tanto de la superclase como de las subclases, pero este no es el caso.
Figura 4.31 Código de la clase Chino La línea 5 de la figura 4.31 hace un llamado al método super(String). La palabra super se utiliza en Java para referirse a la superclase. Cuando se crea un objeto de la clase Chino, se ejecuta el constructor de la clase Chino (línea 4). El constructor de la clase Chino, a su vez llama al constructor de la clase Cliente (Cliente es superclase de Chino). Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
105
Figura 4.32 Código de la clase Italiano
Figura 4.33 Código de la clase Mexicano
Figura 4.34 Código de la clase Mesero con el método sobrecargado atiende()
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
106
La figura 4.34 muestra el código de la clase Mesero con el método atiende() sobrecargado (líneas 4, 7 y 10) de tal forma que el mensaje que se muestra depende del tipo de objeto que se envía como parámetro al método atiende().
Figura 4.35 Código de la clase Main que modela el Restaurante de Comida Internacional
Figura 4.36 Salida de la Ejecución del Código de la figura 4.35
¿Y el Polimorfismo? Polimorfismo?
El ejemplo anterior modela el funcionamiento del Restaurante Internacional sin polimorfismo. Sin embargo, nos servirá para entender el concepto de polimorfismo y también el porqué es un concepto tan importante y útil en la programación orientada a objetos. Primero, veremos el problema de modelar el Restaurante Restaurante de la forma descrita arriba.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
107
Supongamos ahora que el dueño del restaurante decide agregar comida española a sus platillos. ¿Qué tendríamos que hacer para adecuar el código a este nuevo requerimiento? Pues... Tendríamos que agregar otro método atiende(Español)a la clase Mesero. Además. Claro de crear la clase Español la cual por supuesto extiende la clase Cliente. Lo anterior puede ser un problema pues modificar código puede causar errores en un programa que ya había sido probado. Por otro lado, si optamos por esta opción, cada vez que se agregue un tipo de comida al restaurante, será necesario modificar la clase Mesero para agregarle un nuevo método atiende(). Por ejemplo, Si el restaurante llegara a tener 10 tipos diferentes de clientes, entonces ¡Tendríamos que sobrecargar el método m étodo atiende() 10 veces! Otro problema, el cual es igualmente importante, es que ¡El mesero es quien decide que comida debe servir de acuerdo a la nacionalidad! ¿No sería más conveniente que fuera el propio cliente quien decidiera la comida típica de su país? Usaremos polimorfismo para resolver los dos problemas que acabamos de describir. Podemos decir de manera muy simple que el polimorfismo permite que un objeto pueda tomar varias formas. En esta nueva solución, definiremos un solo método atiende(Cliente)para cualquier tipo de cliente. Además, es el propio cliente (o más bien el programador de la clase Cliente) quien decide el tipo de comida comida que se servirá. Para lograr lo anterior, necesitamos que cada subclase de Cliente implemente su propio método en el cuál se especifique que tipo de comida desea que le sirvan. Este método es pideComida(). La figura 4.37 muestra un diagrama UML de clases con otro enfoque al problema del Restaurante de Comida Internacional utilizando polimorfismo. El método atiende(Cliente) de la clase Mesero de la figura 4.37 puede aceptar cualquier tipo de cliente como parámetro. Como puede observarse en la figura, el parámetro cl es de tipo Cliente. Precisamente es ahí en donde radica el polimorfismo, pues el objeto que se envía al método atiende(Cliente) puede tomar varias formas. Puede ser un objeto de cualquiera de las clases Chino, Italiano o Mexicano. Por eso se llama polimorfismo, porque el objeto puede tomar varias formas. La variable cl se llama variable polimórfica porque puede contener objetos de diferentes tipos. También observamos que las clases Chino, Mexicano e Italiano tienen implementado el método pideComida(). Lo anterior significa que hemos dejado la responsabilidad de decidir que comida servir al creador de cada una de las subclases de Cliente y no al de la clase Mesero. Mesero. ¡Esto es muy importante, pues nunca Mesero! Si Si el nunca necesitaremos modificar modificar la clase Mesero! restaurante, en un futuro lejano o cercano desea servir otro tipo de comida: española, francesa, árabe o cualquier otro tipo, sólo será necesario crear la clase Español (o cualquiera de las otras) que será subclase de de Cliente e implementar el método pideComida() para esta nueva clase. Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
108
Figura 4.37 Modelado del Restaurante Internacional Utilizando Polimorfismo
Figura 4.38 El Polimorfismo P olimorfismo en el Restaurante Internacional Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
109
La figura 4.38 ilustra el concepto de polimorfismo para el ejemplo del restaurante. El objeto de la clase Cliente puede tomar cualquiera de las formas ( Mexicano, Chino o Italiano). Cada una de estas “formas” que puede tomar el objeto tiene sus propias preferencias de comida especificadas en el método pideComida() (ver figura 4.37) Las figuras 4.39 a 4.42 muestran el código que implementa el polimorfismo para el modelado del Restaurante Internacional.
Figura 4.39 Código de la clase Cliente con el método abstracto pideComida() La figura 4.40 muestra el código de las clases Chino, Mexicano e Italiano. Por motivos de espacio pusimos las tres clases en la misma figura, pero cada clase se tiene que colocar en un archivo diferente (por esa razón la numeración comienza en cada clase). Como puede verse en la figura 4.40, cada una de las subclases de la clase Cliente implementa el método pideComida(). Esta implementación es obligatoria pues el método pideComida() el cual es heredado de la clase Cliente, es abstracto y por lo tanto es necesario sobre-escribirlo. Si el método no fuera sobre-escrito, el compilador marcaría un error al momento de intentar compilar las clases. La razón de este error fue explicada un poco antes en este mismo capítulo. En la figura 4.40, la línea 9 de la clase Chino y 8 de las clases Italiano y Mexicano llaman al método getNombre()para mostrar el nombre del cliente que está siendo servido en el restaurante. Anteriormente, teníamos que hacer referencia al objeto para poder llamar a este método. Esto puede verse en las líneas 5, 8 y 11 de la figura 4.34. Cuando llamamos a un método de un objeto, necesitamos hacer referencia al objeto, es decir necesitamos usar la sintaxis objeto.metodo(). Sin embargo, en el código de la figura 4.40 omitimos la referencia al objeto. Esto se debe a que cuando cuando un método de un objeto llama a otro método, se asume que ambos ambos
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
110
métodos pertenecen al mismo objeto. Opcionalmente se puede utilizar la palabra this, como ya lo vimos anteriormente, para hacer referencia al objeto.
Figura 4.40 Código de las clases Chino, Italiano y Mexicano que implementan el método pideComida()
La figura 4.41 muestra el código de la clase Mesero. Como puede verse en la figura, el parámetro cl del método atiende(Cliente) es polimórfico. Esto significa que recibe objetos de distintas subclases de la clase con la cual fue declarado y por lo tanto, los métodos que se llamen pertenecerán a las subclases y no a la superclase. superclase. Como lo hemos mencionado anteriormente, la gran ventaja del uso de polimorfismo en el código de la figura 4.41 es que este código no tiene que modificarse cuando se agreguen más
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
111
nacionalidades a las que se pueden atender en el restaurante. Esto es tan importante, que vale la pena mencionarlo repetidamente. Al recibir un objeto del tipo Cliente, la única función del método atiende(Cliente) como puede verse en la línea 5, es llamar al método pideComida() del objeto que se recibió como parámetro.
Figura 4.41 Código de la clase Mesero que utiliza polimorfismo en el método atiende(Cliente)
De esta manera, es responsabilidad del cliente y no del mesero, decidir la comida que se servirá. De hecho, esta forma de modelar el problema representa más fielmente la realidad del funcionamiento de un restaurante. La figura 4.42 muestra el código de la clase Main. La única diferencia entre este código y el de la figura 4.35 está en la forma de declarar las variables en las líneas 10,11 y 12. En el código de la figura 4.42 se crean los objetos declarando las variables ch, mx e it de tipo Cliente pero se llaman a los constructores de las clases Chino, Mexicano e Italiano respectivamente. Esto es algo perfectamente válido pues las clases Chino, Mexicano e Italiano son subclases de la clase Cliente. El método atiende(Cliente) puede recibir cualquiera de las tres variables directamente pues las tres han sido declaradas con la clase Cliente.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
112
Figura 4.42 Código de la clase Main que modela el funcionamiento del Restaurante Internacional
En resumen, el polimorfismo polimorfismo nos permite tratar a los objetos de subclases como como si fueran de la superclase pero al momento de llamar a los métodos de éstos objetos, se ejecutan los métodos sobre-escritos en las subclases y no los de la superclase. Sin embargo, no sucede lo mismo con los atributos. Si la subclase tiene definidos atributos que no pertenecen a la superclase, éstos no serán visibles a menos que se realice una transformación del objeto a la clase con la que fue creado. A esta transformación se le llama “cast” y “cast” y lo veremos más adelante en el libro. Retorno Covariante
Hemos visto que un objeto de cierta clase puede ser enviado como argumento a un método que recibe argumentos de la superclase. Por ejemplo, podemos enviar un objeto de la clase Perro a un método que espera objetos de la clase Animal. Algo similar podemos hacer al regresar objetos en los métodos. El lenguaje Java permite el retorno covariante. Podemos decir de manera muy simple que si un método es sobre-escrito, el tipo de retorno del método de la subclase puede ser a su vez subclase del tipo de retorno del método que se está sobre-escribiendo. Esto se ilustra con un ejemplo en la figura 4.43 4 .43
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
113
Figura 4.43 Ejemplo de Retorno Covariante La clase FabricaFiguras de la figura 4.43 tiene un método hazFigura() que regresa un objeto de la clase Figura (líneas (líneas 5-7). Este método es sobre-escrito en la clase FabricaCuadrado y el objeto que regresa su método hazFigura() (líneas 12 a 14) es de la clase Cuadrado, que es subclase de Figura. Esta característica de los lenguajes orientados a objetos permite que los métodos sobre-escritos tengan más flexibilidad en los tipos de retorno y que ambos métodos (o sea, el de la superclase y el de la subclase) puedan ser de diferentes tipos de retorno. Siempre y cuando el tipo de retorno del método sobre-escrito sea superclase del tipo de retorno del método que sobre-escribe. Un ejemplo de la utilidad de esta característica podría ser que en el ejemplo del videojuego existiera una clase FabricaDeArmas con su método crearArma(). Ésta clase la podríamos especializar en varias clases para fabricar armas específicas, cada una con su respectivo método crearArma() el cual regresaría el tipo específico de arma. Clases Finales
En algunas ocasiones, se puede presentar la necesidad de que el programador de cierta clase no desee que la clase pueda ser extendida. Supongamos por ejemplo, que la clase RifleLaser del ejemplo del videojuego presentado anteriormente es lo suficientemente especializada y no debe ser extendida pues no se desea tener ningún tipo especial de RifleLaser. Entonces, Podemos Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
114
prohibir que una clase pueda ser extendida agregando la palabra final a la declaración de la clase. Para el ejemplo de la clase RifleLaser, el código sería el siguiente: public final class RifleLaser extends Arma {…}
Al hacer lo anterior, sería imposible extender la clase RifleLaser. Por ejemplo, el siguiente código: class RifleLaserGalactico extends RifleLaser {}
Marcaría el error: Cannot inherit from final RifleLaser…
El error está indicando que la clase RifleLaser fue declarada como final por lo tanto no se puede extender. Una de las principales razones para declarar una clase como final es seguridad. Esto es debido a que las clases finales no pueden tener métodos sobre-escritos. Entonces, de esta forma nos aseguramos que los métodos que se ejecutan pertenecen a la clase y no existe la posibilidad de ejecutar un método sobre-escrito por alguna de las subclases. El Ejemplo de la Nómina Revisitado
En esta sección presentaremos nuevamente el ejemplo de la nómina utilizando ahora herencia, polimorfismo y otros conceptos relacionados. Empezaremos describiendo nuevamente el problema con algunos cambios en los requerimientos que harán necesario el uso de los conceptos vistos en este capítulo.
Se desea construir un programa que calcule el pago a los trabajadores de una empresa. Existen dos tipos de trabajadores: obreros y supervisores. La empresa cuenta actualmente con 10 trabajadores (8 obreros y 2 supervisores). Ver la siguiente tabla:
Nombre Armando Paredes Cindy Nero Alan Brito Marco López Rosario Márquez Sergio Martínez
Salario por Hora 60 50 40 55 180 75
Conceptos de Programación Orientada a Objetos
Horas Trabajadas 40 35 48 40 40 38
Tipo
Premio
Obrero Obrero Obrero Obrero Supervisor Obrero
100
Héctor A. Andrade G.
Descuento
50 --150 120 115
Luisa Domínguez Petra Barrera Manuel Flores Alma Ríos
50 55 65 175
30 40 35 40
Obrero Obrero Obrero Supervisor
110 --
--
--
--
--
--
Además de su salario normal, los trabajadores reciben un bono adicional (500 los supervisores y 300 los obreros) el cual se modifica periódicamente pero es el mismo para todos los trabajadores del mismo tipo. Cada obrero puede obtener un premio por productividad mensual el cual es determinado por el supervisor. Los supervisores que han pedido un préstamo se les hace un descuento semanal. Los obreros no pueden pedir préstamos y los supervisores supervisores no tienen premios. La entrada al sistema son las horas trabajadas por semana, el salario por hora, el premio (en el caso de los obreros) y el descuento (en el caso de los supervisores). El programa deberá producir el pago semanal y el total pagado en esa semana. La salida del programa será similar a la mostrada en la figura 4 .44.
Figura 4.44 Nómina Semanal de la Empresa Acme
Análisis del problema.
El problema a resolver es básicamente el cálculo del pago a los trabajadores. Este problema ya lo habíamos resuelto anteriormente. Sin embargo, existen nuevos requerimientos que nos obligan a replantear nuestras clases. A continuación presentamos una lista de ellos: 1. 2. 3. 4.
Ahora tenemos dos tipos de trabajadores: obreros y supervisores Todos los obreros tienen un bono de 300 pesos Todos los supervisores tienen un bono de 500 pesos Los obreros pueden tener premios que varían de acuerdo a su desempeño
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
116
5. Los supervisores pueden tener descuentos por préstamos que les fueron otorgados Para satisfacer el requerimiento uno, podemos crear dos clases que extienden la clase Trabajador: Obrero y Supervisor. Los requerimientos dos y tres los podríamos resolver definiendo atributos de clase (estáticos) para cada clase. Podríamos definir un atributo estático bono para cada una de las clases y de esta manera cuando se cambie el valor del bono solo tendríamos que modificarlo una vez, pues el atributo pertenecería a la clase y no a cada uno de los obreros y supervisores. Los requerimientos tres y cuatro indican que la forma de calcular el pago es diferente para los supervisores que para los obreros. Entonces, podemos utilizar polimorfismo para que un Trabajador pueda tomar la forma de un Supervisor o de un Obrero y que el pago sea calculado de acuerdo a su tipo de Trabajador. Además de las clases mencionadas anteriormente, hemos decidido incluir la clase Impresora que se encargará de imprimir o mostrar la información en la consola de la computadora. Otra clase que consideramos necesaria para un buen diseño es la clase BaseDatos. Ésta clase se encargará de cargar la información al arreglo de trabajadores. Finalmente, la clase Main que arranca la ejecución del programa. La figura 4.45 muestra las clases que utilizaremos y una breve explicación de sus “responsabilidades” o sea cuáles son sus funciones dentro del programa. programa. Es generalmente una buena idea hacer un diagrama que muestre de manera preliminar cuales serán las clases de nuestro diseño. Puede describirse también en este diagrama las responsabilidades que tendrán dentro del programa así como las relaciones entre ellas. Como puede observarse en la figura 4.45, no todas las clases que se implementarán en el programa fueron mencionadas en la definición del problema. Por ejemplo, las clases Impresora, Main y BaseDatos no pertenecen realmente al dominio del problema. Decidimos incluirlas porque, para este problema, existen funciones que no quedan muy bien en ninguna de las clases del mundo real. Por esta razón, las colocamos en el paquete etc como se muestra en la figura 4.46. Lo contrario también es posible. Podríamos tener clases mencionadas en el problema que no son necesarias en diseño de la solución del mismo. Por ejemplo, no consideramos necesario incluir la clase Empresa en el diseño de la solución de este problema.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
117
Figura 4.45 Clases y sus Responsabilidades Responsabilidades para el Sistema de Nómina La figura 4.46 muestra el diagrama de clases de la solución al problema de la nómina. Hemos estado introduciendo más notaciones a los diagramas UML. Es muy importante dominar la nomenclatura de este lenguaje pues con él se expresan prácticamente todos los conceptos que hemos visto hasta ahora. Además, es un lenguaje universalmente aceptado para expresar tanto los dominios de los problemas como las soluciones a los mismos. En la figura 4.46 podemos ver las clases que intervienen en la solución del problema, así como sus relaciones, los atributos y métodos, los permisos de acceso a los mismos, m ismos, los atributos y métodos estáticos, etc. Algo importante que debemos discutir sobre el diseño presentado en la figura 4.46 es que algunas clases sólo contienen métodos estáticos como por ejemplo Main, Impresora y BaseDatos. Una pregunta que podemos hacernos es… ¿Cuándo debemos utilizar métodos estáticos? ¿Hay alguna ventaja? ¿Qué pasaría si en lugar de métodos estáticos (o sea, de clase) hubiésemos utilizado métodos de instancia? Trataremos de responder a estas preguntas pero antes debemos aclarar que la decisión del uso de métodos estáticos tiene que ver con eficiencia y con elegancia Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
118
en el diseño y NO con funcionalidad. Lo que significa esto es que podríamos omitir en la mayoría de los casos los métodos estáticos y utilizar solamente métodos de instancia y viceversa. En este caso en particular, no es necesario crear un objeto de la clase Impresora porque realmente la impresora no tiene datos ni atributos que sean útiles y lo mismo pasa con las clases Main y BaseDatos. Por otro lado, las clases Obrero y Supervisor sí contienen datos de cada trabajador. Por lo tanto, cada trabajador maneja datos diferentes (sus propios datos) y es conveniente que los métodos que accedan a éstos datos sean de instancia y no estáticos. Respondiendo a la segunda pregunta. Si utilizáramos métodos de instancia en estas clases, entonces tendríamos que crear los objetos para poder utilizarlas, lo cual sería ineficiente pues los objetos estarían ocupando un lugar en la memoria sin que realmente se utilice.
Figura 4.46 Diagrama de Clases del Diseño del Programa de Nómina Las figuras 4.47 a 4.52 contienen los códigos de las clases mencionadas anteriormente. Cada clase es pública, por lo que deberá estar grabada en un archivo con el nombre de la clase y la extensión java. Cómo los códigos están en dos diferentes paquetes, se tienen que crear sendos subdirectorios, uno por cada paquete.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
119
Figura 4.47 Código de la clase Trabajador del Ejemplo de la Nómina
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
120
Figura 4.48 Código de la Clase Supervisor
Figura 4.49 Código de la Clase Obrero
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
121
Figura 4.50 Código de la Clase BaseDatos En la figura 4.51 podemos observar que el método imprimeDetalle(Trabajador) de la clase Impresora, recibe un objeto de la clase Trabajador. Esto puede puede parecer un poco extraño pues hemos mencionado en repetidas ocasiones que las clases abstractas no son instanciables (o sea,que no se pueden crear objetos de ellas). En este caso, la clase Trabajador es abstracta, entonces… ¿Cómo es posible que el método imprimeDetalle(Trabajador) reciba un objeto de la clase Trabajador? La realidad es que el objeto NO es propiamente de la clase Trabajador. Si observamos la figura 4.50 en las líneas 10 a 19 vemos que cada objeto es creado ya sea como Supervisor o como Obrero. Lo que ocurre es que el método imprimeDetalle(Trabajador) recibe al objeto en esa forma pero no significa que el objeto en si, sea de la clase Trabajador. Sería como una especie de “rol” rol” que el objeto juega para ese método. Cómo hemos visto anteriormente, un objeto puede tener varios roles y diferentes métodos pueden recibir al mismo objeto en diferentes roles. Algo similar sucede en la realidad cuando un objeto, por ejemplo un cuchillo puede tener el rol de utensilio de cocina, pero también puede usarse como una herramienta para atornillar.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
122
Figura 4.51 Código de la Clase Impresora
Figura 4.52 Código de la Clase Main Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
123
Resumen del Capítulo
En este capítulo estudiamos el concepto de herencia, el cual es muy importante en la Programación Orientada a Objetos. Es fundamental entender la herencia y todos los conceptos relacionados para poder escribir programas correctos y eficientes. La herencia está asociada también al permiso de acceso acceso protegido, al polimorfismo, polimorfismo, a las clases abstractas, abstractas, a las clases finales, a la sobre-escritura de métodos y a muchos otros conceptos de gran relevancia para la programación. Básicamente, podemos decir que la herencia es un mecanismo de reúso de código en el cual una clase se especializa (se agrega información) para que pueda ser utilizada por un programa particular. La relación entre la clase original y la clase derivada es un a relación “es un” o sea, la clase derivada o subclase “es un” tipo particular de la superclase. Por ejemplo, un automóvil “es un “vehículo, un obrero “es un” trabajador, etc. Vimos detalladamente cómo funciona el polimorfismo y en qué casos puede ser aplicado. El polimorfismo nos permite ahorrar grandes cantidades de código pues podemos escribir métodos con argumentos polimórficos. Es decir, argumentos que pueden tomar varias formas. Esto permite que escribamos código más robusto pues como vimos en los ejemplos, cada subclase implementa sus propios métodos declarados en la superclase, la cual normalmente es una clase abstracta. Las clases abstractas son clases no instanciables. Es decir, no se puede crear objetos a partir de ellas. Sin embargo, su utilidad radica en que sirven de base para crear otras clases. Otra de las facilidades que nos otorga el lenguaje Java en la sobre-escritura de métodos es el retorno covariante, en el cual, podemos sobre-escribir métodos cuyo tipo de retorno puede ser una subclase del tipo de retorno del método que se está sobre-escribiendo. Finalmente, vimos que al agregarle la palabra final a la declaración de una clase, ésta ya no puede ser instanciada.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
124
Ejercicios y preguntas.
1. Describa la relación entre el concepto de Herencia H erencia y el de Polimorfismo 2. EL siguiente código marca error. Explique la razón por la que este código no debe compilar public final abstract class X {}
3. Cuáles de las siguientes parejas de Clase – Clase – Subclase Subclase serían correctas de acuerdo al significado de sus nombres. Llenar la tabla con Correcto o Incorrecto según sea el caso: SUPERCLASE
Vehículo Automóvil Padre Color Computadora Mujer Máquina Perro América
SUBCLASE
CORRECTO / INCORRECTO
Avión Llanta Hijo Verde Teclado Hombre Motor Animal México
4. Escriba el mínimo código en Java que implemente el siguiente diagrama de clase UML:
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
125
5. Dibuje un diagrama UML de clases que represente el siguiente código:
6. Escriba una clase base (superclase) que contenga los atributos y métodos propios de un Vehículo. ¿La consideraría abstracta? 7. Resuelva el siguiente problema utilizando herencia, polimorfismo y colecciones heterogéneas. Dibuje un diagrama UML de clases de su solución e implemente el código de tal forma que concuerde con el diagrama. Una escuela desea calcular el pago de sus Profesores, los cuales son considerados como objetos de tipo Pagable. Hay tres tipos de Profesores: Profesores de Tiempo Completo .
A estos profesores se les paga un salario fijo Profesores de asignatura
A estos profesores se les paga por hora (horas trabajadas por salario_hora) Profesores con cargo adminstrativo
A estos profesores se les paga un salario fijo más un bono de 1000 pesos quincenales Pruebe su sus clases con el siguiente código (No debe modificar este código en absoluto):
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
126
class ExamenPoli { public static void main(String[] args) { Pagable[] profesores = { new ProfesorTiempoCompleto("Juan López", 15000), new ProfesorAsignatura("Nury Mercado", 30, 200), new ProfesorAdministrativo("Mariano Torres", 15000, 2000) }; System.out.println("**Pago de Nómina**"); System.out.printf("%-15s\t%10s\n", "NOMBRE", "PAGO"); System.out.println("--------------------------------"); for (Pagable p : profesores) { float pago = p.calculaPago(); System.out.printf("%-15s\t%10.2f\n", ((Profesor) p).getNombre(), pago); } } }
La salida del programa deberá ser la siguiente: **Pago de Nómina** NOMBRE PAGO --------------------------------------Juan López 15000.00 Nury Mercado 6000.00 Mariano Torres 17000.00
8. Describa el concepto de retorno covariante y escriba un ejemplo de código Java que lo implemente. 9. Llene la siguiente tabla con los permisos de acceso. Escriba una P si se puede acceder o una N si no se puede acceder a los miembros de la clase. La columna Clase se refiere al acceso a los miembros desde la misma clase. La columna Paquete se refiere al acceso desde las clases que se encuentren en el mismo paquete. La columna Subclase se refiere al acceso desde cualquier subclase que esté o no en el mismo paquete y la columna Cualquiera se refiere a cualquier clase situada en cualquier paquete.
Permisos de Acceso Tipo
Clase Paquete Subclase Cualquiera
public protected protected (default) private private
10. Describa el concepto de Herencia Múltiple. Explique el problema del Diamante Mortal y Cómo Java elimina este problema a través del uso de interfaces. 11. Considere el siguiente código:
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
127
¿Cuál(es) líneas son erróneas? Explique el porqué (si las hay)
12.- Escriba el código que implemente completa y correctamente el diagrama de clase mostrado:
Pruebe su código con el siguiente código de la clase Main:
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
128
La salida deberá ser la siguiente: Area del circulo:78.53982 Perimetro del círculo:31.415928 Area del circulo:12.0 Perimetro del círculo:14.0
13.- Explique por qué el uso de clases finales hace más seguro el funcionamiento de los programa en Java. Muestre un ejemplo con la clase String (la cual es final). ¿Qué escenario inseguro podría ocurrir si la clase String no fuera final? 14. Escriba un programa que implemente las siguientes las siguientes clases: • • • • • •
Clase abstracta Mueble Interface Armable Interface Pintable Clase concreta Silla Clase concreta Mesa Clase concreta Banco Clase concreta Main
El método public static main(String[]) deberá crear un arreglo (colección heterogénea de muebles) y un método estático void construye(Mueble) que imprima las instrucciones para construir el mueble de acuerdo a su tipo. La salida deberá ser similar a esta: Mueble: Silla Colonial Tipo de mueble: Silla Instrucciones de armado: Pegar las patas al asiento y pegar el respaldo Pintar con brocha usar barniz rebajado al 70% Mueble: Mesa de Comedor para seis personas Tipo de mueble: Mesa Instrucciones de Armado: Pegar las patas a la superficie y reforzar Pintar con pistola de aire y barniz rebajado al 50% … …
NOTAS: Utilice polimorfismo Las clases concretas implementan dos interfaces Deduzca los métodos adecuados de las interfaces Deduzca los constructores adecuados de las clases 15.- Cual es la salida del siguiente código (Ejecútelo mentalmente)
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
129
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
130
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
131
5. Excepciones y Flujos Avanzados de Datos En este último capítulo veremos dos temas que completan el curso básico de Programación Orientada a Objetos. Primero discutiremos el manejo de Excepciones, el cual es un mecanismo para especificar a la computadora que hacer en caso de que suceda una situación extraordinaria durante la ejecución de un programa. Posteriormente, veremos el m anejo de Flujos Avanzados de Datos y particularmente estudiaremos el manejo de archivos los cuales nos permiten almacenar los objetos de manera permanente en la memoria secundaria de la computadora. Excepciones
Como fue mencionado en el párrafo anterior, una excepción es una situación extraordinaria que puede pasar cuando se ejecuta un programa. Las excepciones sólo pueden suceder cuando se ejecuta un método de un objeto o de una clase. Cuando un objeto realiza una función, es posible que suceda algo que no está dentro del cauce natural de cosas que pueden suceder. Veamos por ejemplo la figura 5.1. El padre está ordenando a su hijo que vaya a la tienda a comprar un refresco.
Figura 5.1 Ejemplo de Excepciones Al ordenar la compra de un refresco, existen varias excepciones que pueden ocurrir. Como lo muestra la figura 5.1, el hijo pregunta al padre que hacer en caso de que la tienda esté cerrada o que no le alcance el dinero para comprar el refresco. Existen muchas otras excepciones en las que
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
132
podemos pensar en esta situación. Por ejemplo, que no haya el tipo de refresco deseado, o que existan varias presentaciones, etc. En general, se deben plantear las excepciones más comunes o probables de acuerdo a la experiencia del programador. Algo muy similar sucede en programación. Cuando un objeto está ejecutando una función (método) es posible que suceda una situación extraordinaria. Por ejemplo, supongamos que un objeto tiene un método que calcula la raíz cuadrada de un número que se envía como argumento. Una posible excepción podría ser que el argumento sea un número negativo, el cual no tiene un número real que sea su raíz cuadrada. Manejo de Excepciones en Java.
En Java, las excepciones son objetos de la clase Exception la cual se encuentra en el paquete java.lang. Aunque pueden pertenecer también a alguna de sus subclases. Existen varias clases de excepciones predefinidas en el mismo paquete. Todas terminan con el sufijo “ Exception”. La figura 5.2 muestra algunas excepciones predefinidas y la situaciones en las cuales ocurren.
Figura 5.2 Algunas Excepciones predefinidas en el paquete java.lang En la figura 5.2 vemos que existen básicamente dos tipos de excepciones predefinidas: Las que ocurren al ejecutar instrucciones de entrada o salida ( IOException) y las que ocurren al ejecutar cualquier otro tipo de instrucción (RunTimeException). Existen otras excepciones predefinidas pero en este libro sólo discutiremos las mostradas en la figura. Algo interesante es que nosotros podemos crear nuestras propias excepciones. Esto lo veremos a continuación.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
133
Cláusulas “Throws” y “Throw”.
Regresemos al ejemplo de la compra del refresco. Simularemos esta situación con las clases Padre e Hijo. El padre invocará al método compraCoca() del hijo. El método compraCoca() puede generar (o “aventar”) dos posibles excepciones. Cada excepción es un objeto que en este caso puede ser de la clase TiendaCerradaException o de la clase DineroInsuficienteException. La figura 5.3 muestra el código de la clase Hijo. Existen varias cosas importantes que debemos entender del código de la figura 5.3. La línea 7 muestra el encabezado del método compraCoca(). En esta línea y en la siguiente, se declara que el método compraCoca() puede aventar dos tipos de excepciones TiendaCerradaException y DineroInsuficienteException. Nótese que enfatizamos puede porque no necesariamente aventará alguna de las dos excepciones como se ve más adelante en el código. La forma de declarar que un método puede aventar una excepción, es a través del uso de la palabra throws (que significa “avienta” en Inglés). Cuando se utiliza la palabra throws en la declaración de un método para indicar que el método puede aventar una excepción, esto obliga al método llamador a manejar la excepción a menos que la excepción no pertenezca a la clase Exception como veremos más adelante.
Figura 5.3 Código de la clase Hijo Otra cosa que debemos observar de la figura 5.3 es que se está simulando las diferentes situaciones extraordinarias a través del uso de un número aleatorio generado a por el método estático random() de la clase Math la cual ya viene incluida en el paquete java.lang por lo que no es necesario hacer el import para esta clase. El método random() genera un número Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
134
de tipo double mayor o igual a 0 y menor que 1 con una distribución de probabilidad uniforme (o sea con igual probabilidad para cualquier número). Se asume entonces que hay un tercio de probabilidad para cada rango (o sea, que el número sea menor que 0.33, que esté entre 0.33 y 0.66, y que sea mayor que 0.66). Si el número generado es menor que 0.33, entonces se ejecuta la instrucción en la línea 18: throw(new TiendaCerradaException()); TiendaCerradaException());
El throw es una instrucción que provoca que el método que la ejecuta, en este caso compraCoca(), aviente la excepción. excepción. ¿A quién se la avienta? avienta? Al método que haya haya llamado al método compraCoca(). Más adelante veremos que puede hacer un método cuando ocurre una excepción dentro de su ejecución. Otra cosa interesante que observamos en la línea 18 es que estamos enviando una excepción a través de la cláusula throw que consiste en un objeto que ¡Estamos creando al vuelo! Este tipo de objetos que son creados al vuelo se les llama objetos anónimos. ¿Por qué anónimos? Porque… ¡No tienen nombre! Es muy común este tipo de instrucciones en situaciones en las que un objeto sólo será referenciado una sola vez. Es importante señalar que tanto la cláusula throws de la línea 7 como el método throw de las líneas 18 y 19 sólo con pueden ser usados con subclases de la clase Exception. Cláusulas “try” y “catch”.
La figura 5.4 muestra el código de la clase Padre. Las líneas 3 y 4 importan las dos clases correspondientes a las excepciones declaradas en el método comprarCoca(). Nótese que se tiene que importar ambas clases porque están en otro paquete. Por otro lado, no es necesario importar la clase Hijo porque está en el mismo paquete (familia).
Figura 5.4 Código de la Clase Padre
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
135
Cuando un método o una instrucción pueden aventar una excepción de la clase Exception o alguna subclase de Exception, es necesario que se “encierre” este método o instrucción en un bloque precedido por la palabra try. Las línea 9 a 11 muestran el bloque que encierra el llamado al método compraCoca() de la clase Hijo. Debido a que en ese caso, el método compraCoca() puede aventar dos tipos de excepciones, entonces tenemos que escribir dos bloques precedidos por la palabra catch inmediatamente después de terminar el bloque try. Las líneas 11 a 14 y 14 a 17 de la figura 5.4 muestran estos dos bloques, uno para cada tipo de excepción que el método pueda aceptar. La sintaxis para el manejo de excepciones entonces es la siguiente: try { //Métodos o instrucciones que pueden generar alguna excepción } catch (ClaseDeLaExcepcion e) { //Código que debe ejecutarse cuando se genera una excepción }
Las instrucciones que se escriben dentro del bloque try son las que provocan las excepciones y las que se escriben dentro del bloque catch son las que manejan las excepciones. Regresando al ejemplo de la compra del refresco. Si tuviéramos que traducir al español lo que está escrito en las líneas 9 a 17, sería una conversación más o menos como la siguiente: Padre: “Por favor ve a comprar una coca a la tienda” tienda” (línea 10) Hijo: “¿Qué hago si la tienda está cerrada?” (línea 11) Padre: “Ve a al súper” (líneas 12 y 13) Hijo: “¿Y si no tengo suficiente dinero?” (línea 14) Padre: “Pídele a tu mamá” (líneas 15 y 16) Cuando se ejecute el código de la figura 5.4, como ya se explicó anteriormente, las tres situaciones (tienda cerrada, dinero insuficiente y ningún problema) serán emuladas con un número aleatorio generado en el método compraCoca() de la clase Hijo. Más adelante veremos un ejemplo en el cual la excepción ocurre en realidad. Finalmente, la Figuras 5.5 y 5.6 muestran los códigos de las clases TiendaCerradaException y DineroInsuficienteException respectivamente. En ambos códigos podemos ver que las clases extienden a la clase Exception. Esto es necesario para crear excepciones que obliguen a los programadores a utilizar bloques try – catch para manejarlas.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
136
Figura 5.5 Código de la clase TiendaCerradaException
DineroInsuficienteException teException Figura 5.6 Código de la Clase DineroInsuficien
Al ejecutar el código de la clase Padre mostrado en la figura 5.4 varias veces, tendremos diferentes resultados como lo muestra la figura 5.7.
Figura 5.7 Resultado de la ejecución del código de la clase Padre (fig. 5.4) Otra forma de manejar las Excepciones
Como hemos visto hasta ahora, las excepciones deben ser manejadas por el método llamador al que generó la excepción. En el ejemplo de la compra de refresco, quien maneja las excepciones es el método main(String[]) de la clase Padre. Esto es, quien decide que hacer para manejar Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
137
las excepciones es este método y no el método compraCoca() de la clase Hijo. Resumiendo, el método compraCoca() avienta la excepción y el método main(String[]) la “atrapa” atrapa” (por eso el nombre del bloque catch). Sin embargo, embargo, supongamos que el el programador del método llamador no desea manejar la excepción. ¿Existe alguna forma de no manejar la excepción sin que marque error el compilador? La respuesta, obviamente, es sí (si no, ¡No hubiéramos escrito la pregunta!). Podemos ver el manejo de excepciones de manera similar como se manejan los conflictos en una organización. organización. Supongamos que existe un conflicto conflicto en un departamento de una organización. El jefe del departamento puede manejar el conflicto (excepción) o puede pasársela a su jefe. Este a su vez puede manejar el conflicto o pasárselo a su jefe y así sucesivamente hasta llegar a la máxima autoridad de la organización quien tiene la obligación de manejar todos los conflictos. La figura 5.8 5 .8 ilustra este concepto. El manejo de excepciones es muy similar al ejemplo de la figura 5.8. Cuando ocurre una excepción en un método, este puede manejarlo de dos formas: Utilizando las cláusulas try-catch ó puede simplemente aventarlo al método llamador utilizando la cláusula throws en la declaración del método. Si el programador está absolutamente seguro como debe manejar la excepción, pues entonces no tiene sentido aventarla al método llamador. Lo mismo que pasa en la organización, si el jefe del departamento sabe cómo manejar el conflicto, no tiene caso que lo pase a su superior. Sin embargo, no se debe manejar un conflicto (o excepción) si no se tiene la seguridad plena de que se sabe cómo manejarla correctamente.
Figura 5.8 Analogía del Manejo de Excepciones con el Manejo de Conflictos Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
138
Algo más sobre la analogía de la figura 5.8. Es posible que algunas excepciones puedan ser manejadas por el jefe y otras no. De igual forma, podemos manejar algunas excepciones utilizando utilizando la cláusulas try-catch y otras con la cláusula throws como veremos en los ejemplos posteriores. Ejemplo de la Compra del Refresco Revisitado.
Para ilustrar los conceptos vistos arriba sobre el manejo de excepciones vamos a extender el ejemplo de la compra del refresco introduciendo una nueva clase ( Madre) la cual podrá manejar algunas excepciones dentro de su método, pero otras las aventará al método llamador. La idea está ilustrada en la figura 5.9.
Figura 5.9 Ejemplo de Excepciones La madre de la figura 5.9 puede manejar la excepción del dinero insuficiente pues ella lo tiene. Sin embargo no sabe cómo manejar el problema de que la tienda está cerrada. Entonces, utiliza los dos tipos de manejo de excepciones. Aventará la excepción TiendaCerradaException y resolverá la excepción DineroInsuficienteException. De esta forma, el padre sólo tiene
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
139
que preocuparse por la primera. El ejemplo completo del código de las clases Madre y Padre está en las figuras 5.10 y 5.11. El código código de la case Hijo permanece igual al de la figura 5.3 al igual que la salida del programa.
Figura 5.10 Código de la clase Madre
Figura 5.11 Código de la clase Padre
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
140
Propagación de Excepciones.
Existen varias cuestiones que aún es necesario discutir para tener un conocimiento más completo sobre el manejo de excepciones. Por ejemplo, ¿Qué pasa cuando una excepción no es atrapada? Consideremos el código de la figura 5.12
Figura 5.12 Código de la clase EjemploExcepcionesSinAtrapar Al compilar y ejecutar el código de la figura 5.12 se produce la salida mostrada en la figura 5.13. El código de la figura 5.12 contiene una instrucción que genera una excepción. Ésta excepción ocurre cuando se intenta realizar una división entera con el divisor igual a 0. Como puede verse en la figura 5.12, no existe ningún bloque try-catch ni tampoco una cláusula throws en la declaración de ningún método. Una pregunta que surge inmediatamente es ¿Porqué el código compila sin errores si hay una instrucción que genera una excepción? La respuesta es muy simple. No todas las excepciones deben ser atrapadas. En la figura 5.2 vemos que hay una clase de excepción llamada RunTimeException. Ni esta excepción, ni sus subclases obligan a escribir bloques try-catch o cláusulas throws al programador. A este tipo de excepciones se les llama “Excepciones sin Checar” (Unchecked Exceptions). Por otro lado, las excepciones que pertenezcan a la clase Exception o alguna subclase son “cachables” (Checked Exceptions). Es decir, deben ser atrapadas o el programa no compila.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
141
Figura 5.13 Resultado de ejecutar el código de la figura 5.12 Cuando una excepción no es atrapada, el programa termina de manera abrupta, mostrando un mensaje de error en la consola de errores (la cual normalmente es la misma consola de salida del programa). La salida mostrada en la figura 5.13 es precisamente el mensaje de error que indica que el programa tuvo una excepción y por lo tanto no puede continuar. Como puede verse en la figura 5.13 se muestra la excepción ocurrida, el método en el cual ocurrió, el número de línea, la clase e incluso el nombre del archivo que contiene dicha clase. Vemos que no solo muestra un método sino varios métodos con sus números de líneas. Esto es porque cuando ocurre una excepción, esta se propaga desde el método en el cual ocurrió hacia el método llamador, y después hacia el llamador del llamador y así sucesivamente hasta llegar al método main(String[]) el cual es el método que inicia la ejecución de todos los programas. El texto mostrado en la figura 5.13 se le llama pila de ejecución (execution stack) porque cada vez que un método llama a otro se almacenan en una pila en la cual el orden es inverso al orden del llamado. Observe que el último método mostrado es el main(String[]) cuando en realidad fue el primero en ser llamado. Otro punto interesante que debemos observar es que aunque la línea 5 de la figura 5.12 contiene la instrucción System.out.println("Hola") el mensaje no se muestra en la salida del programa. Esto se debe a que cuando ocurre una excepción, el programa detiene su ejecución normal. En este caso particular, al ocurrir la excepción en la línea 15 se provoca una excepción en la 11, la cual provoca una excepción en la 8, la cual a su vez provoca la excepción en la línea 5. Como la excepción no es atrapada, entonces se detiene la ejecución del programa y no se ejecuta la instrucción que imprime el mensaje. Es decir, el programa “aborta” antes de ejecutar la instrucción de impresión.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
142
La figura 5.14 muestra un código muy similar pero en este caso hemos agregado instrucciones para “atrapar” la excepción. excepción.
Figura 5.14 Código de la clase EjemploRuntimeException En el código de la figura 5.14, hemos agregado instrucciones para atrapar y manejar la excepción mediante bloques try-catch (líneas 5 y 8). Al ejecutar el código, vemos que ahora el programa termina su ejecución normal pero aún no muestra el mensaje de la línea 6. La salida del programa es simplemente: / by zero
Es decir, sólo se muestra el mensaje de la excepción. Esto es debido que cuando atrapamos una excepción, se ejecuta el código del bloque catch y se continúa con el código siguiente al bloque catch. Si quisiéramos mostrar el mensaje “Hola” “Hola” aunque suceda le excepción, excepción, debemos colocar la instrucción de impresión después del bloque catch. Esto se muestra en la figura 5.15.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
143
Figura 5.15. Código de la clase EjemploRuntimeException. Finalmente… Finally
En ocasiones, es necesario que algunas instrucciones se ejecuten sin importar si hubo o no excepción o inclusive si dentro del bloque catch, hubo otra excepción o un return. Veamos otro ejemplo de código en la figura 5.16.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
144
Figura 5.16 Código de la clase EjemploRuntimeException que no muestra el mensaje A pesar de que pusimos la instrucción de impresión después del bloque catch (línea 12 de la figura 5.16), el return de la línea 10 impide que se ejecute la línea 12 pues termina la ejecución del método main(String[]). Entonces… podemos preguntarnos ¿Existe alguna ¿Existe alguna forma de que se ejecuten instrucciones sin importar si hubo o no excepciones, o returns dentro de las excepciones? Obviamente la respuesta es sí. El lenguaje Java permite incluir el bloque finally el cual SIEMPRE se ejecuta. Más adelante veremos que esto es muy útil cuando se manejan archivos y otro tipo de flujos avanzados de datos. El uso del bloque finally se muestra en la figura 5.17. En la figura 5.17 observamos que se ha agregado un bloque finally (líneas 12, 13 y 14). Este bloque imprime el mensaje importante que debe ser impreso sin importar lo que ocurra dentro del bloque catch. En realidad, el único caso en el cual el bloque finally no sería ejecutado sería si se hubiera terminado la ejecución del programa dentro del bloque catch. Esto es, si se escribe la instrucción system.exit(1) en lugar del return en la línea 10. Un último ejemplo de Excepciones.
Los ejemplos vistos hasta ahora ilustran el manejo de excepciones pero no son muy realistas pues primero simulamos la situación de la excepción mediante un número aleatorio y posteriormente fabricamos la excepción al dividir por cero. En este último ejemplo de excepciones presentaremos un caso más realista con una situación que puede presentarse al manejar una cuenta bancaria. Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
145
Supongamos que tenemos que representar una cuenta bancaria la cual tiene un saldo y operaciones para depositar y retirar dinero de la cuenta. El código se muestra en la figura 5.18.
Figura 5.17 Código de la clase EjemploRuntimeException con el uso de finally
Figura 5.18 Código de la clase CuentaBancaria
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
146
La línea 10 de la figura 5.18 se encarga de aventar una excepción cuando se intenta realizar un retiro de una cantidad mayor al saldo. Obviamente esto no debe ser permitido pues el saldo quedaría negativo. La figura 5.19 muestra el código de la clase Cliente que hace uso de la excepción para terminar una serie de retiros a la cuenta bancaria.
Figura 5.19 Código de la clase Cliente que utiliza excepciones para terminar su ejecución Al ejecutar el código de la figura 5.19 obtenemos la salida mostrada en la figura 5.20.
Figura 5.20 Salida de la ejecución del código de la figura 5.19 Note que aún falta crear la clase SaldoInsuficienteException que se encuentra en el paquete excepciones. Pero esto lo dejamos al lector como ejercicio. El manejo de excepciones permite que los códigos sean más “limpios” pues no tenemos la necesidad de agregar “ifs” para el manejo de las diversas situaciones que se puedan presentar. Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
147
Además muchas excepciones son estándar y pueden ser aventadas por diferentes métodos. Por otro lado, las excepciones pueden almacenar información contextual sobre el error o situación extraordinaria que se presentó. En los ejemplos mostrados nosotros sólo utilizamos el mensaje de la excepción, pero podríamos almacenar cualquier tipo de información sobre el error. Después de todo, las excepciones son objetos con atributos y métodos. En la siguiente sección veremos el uso de flujos avanzados de datos en los cuales usaremos otro tipo de excepciones. Flujos Avanzados de Datos.
Hasta ahora, hemos trabajado totalmente en la memoria de la computadora. Es decir, todos los datos asociados con los objetos permanecen en la memoria de la computadora. Esto tiene sus ventajas, pues los accesos a los datos son más rápidos y no es necesario usar instrucciones para guardar o leer datos desde los dispositivos de almacenamiento (discos duros, memorias flash, cintas, etc.). Sin embargo, también existen varias desventajas, La primera es que los datos se borran cuando se apaga la computadora. La segunda es que la memoria principal de las computadoras es más pequeña que la memoria permanente proporcionada por los dispositivos de almacenamiento. Otra desventaja es que, en general, la memoria principal es más costosa. En los lenguajes de programación, tenemos la posibilidad de almacenar los objetos en la memoria permanente. En el lenguaje Java se hace a través del uso de flujos avanzados de datos. Un flujo avanzado de datos (o “Stream” Stream” como se le conoce en I nglés) es simplemente una fuente (o un destino) de la cual (o a la cual) viajan datos. Ver figura 5.21
Figura 5.21 Flujos Avanzados de Datos Los flujos avanzados de datos (les llamaremos simplemente flujos por comodidad) pueden ser de entrada o de salida. En la figura 5.21 vemos que los flujos están ligados a los dispositivos de entrada/salida de la computadora. Por ejemplo, tenemos flujos de entrada para el teclado, el disco duro e incluso podemos tener asociado un flujo de entrada a un punto de conexión en una red de Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
148
computadoras. Por otro lado, tenemos flujos de salida asociados a impresoras, consolas, discos duros y redes. Los flujos de entrada (los cuales son objetos por supuesto) tendrán atributos y métodos que permitan leer datos de ellos. Por otro lado, los flujos de salida (también objetos) tendrán atributos y métodos que nos permitan escribir en en ellos. Una de las características más interesantes de los flujos de datos es que permiten tratar de manera uniforme las operaciones de entrada salida sin importar de qué tipo de dispositivos se trate. Por ejemplo, podemos asociar un mismo tipo (o sea, una misma clase) de flujo de entrada a un archivo que está almacenado en un disco o a la consola de la computadora. Entonces cuando efectuemos una lectura, estaremos leyendo información del disco para el caso del archivo o estaremos leyendo caracteres introducidos usando el teclado para el caso de la consola. Desde el punto de vista del programa que lee los datos, ambos dispositivos se tratan igual, es decir ambos son fuentes de datos de entrada. Esto quedará mas claro con los ejemplos que veremos más adelante. Sabemos que la unidad básica de almacenamiento en las computadoras es el bit (digito binario). Los bits se agrupan agr upan en bytes para formar caracteres (letras, números y caracteres especiales). Para el manejo de flujos, consideraremos adicionalmente que los bytes se agrupan para formar palabras y las palabras se agrupan para formar líneas. Veremos que existen flujos que nos permiten leer o escribir byte por byte, carácter por carácter o línea por línea. También existen flujos que nos permiten leer o escribir objetos. Flujos de Bytes
Los flujos que leen y escriben bytes (secuencias de 8 bits) son subclases de las clases InputStream y OutputStream respectivamente y se encuentran en el paquete java.io. Normalmente, cuando se trabaja con flujos de bytes se hace de manera secuencial desde el primer byte del flujo pasando por cada byte del mismo hasta llegar al último byte. A continuación presentamos un ejemplo de un programa que copia archivos, los cuales son un caso especial de flujos de datos. datos. Las clases clases que que se usan en el ejemplo ejemplo son son FileInputStream y FileOutputStream para leer y escribir datos en archivos en el disco de la computadora. La figura 5.22 muestra la idea general del algoritmo. Como puede observarse en la figura 5.22, el algoritmo para copiar un archivo byte por byte es muy simple. Consiste en ir leyendo cada byte del archivo de entrada y escribirlo en el archivo de salida. Los archivos tienen asociado un apuntador que indica la posición del siguiente byte que se va a leer. Cada vez que se lee un byte (llamando al método read()) se avanza al apuntador para no leer dos veces el mismo byte. Al principio, cuando el archivo se acaba de abrir, el apuntador (a veces también se le llama cursor) apunta al primer byte. Cuando se intenta leer un byte y ya no hay más que leer, entonces el método de lectura regresa un -1. Obviamente, el número -1 no puede formar parte del archivo porque solo lee bytes cuyo rango es de 0 a 255.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
149
Figura 5.22 Algoritmo para copiar un archivo byte por byte La figura 5.23 muestra paso por paso como se copia el archivo con el algoritmo mostrado. Asumimos que cada dígito es un byte. Cada paso de la figura indica el estado de los archivos y el apuntador antes de leer y escribir en cada uno en cada iteración del ciclo while. Así, en el paso 1, se acaban de abrir ambos archivos y el apuntador del archivo “entrada.txt” apunta al primer byte. Aún no se ha escrito nada en el archivo “salida.txt”. El paso 2 muestra el e stado de los archivos después de haber leído el primer byte y haberlo escrito en el archivo de salida. El paso 3 muestra el estado cuando se han leído dos bytes y se han escrito en el archivo de salida. Finalmente en paso 4 muestra el estado cuando se leyeron los tres bytes del archivo de entrada y se han escrito los tres bytes en el archivo de salida. La L a siguiente lectura que se realice en el archivo de entrada ya no regresará un byte sino un valor de -1 indicando que ya no se pueden leer más bytes.
Figura 5.23 Funcionamiento del algoritmo para copiar bytes
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
150
La figura 5.25 muestra el código completo de la clase CopiaBytes que implementa el algoritmo mostrado. Para que el código funcione correctamente, se deberá primero crear el archivo “entrada.txt”. Se puede hacer con el block de notas o cualquier otro programa. Este archivo debe estar situado en el directorio en el cual se corre el programa. La figura 5.25 muestra de manera gráfica el proceso de compilar y ejecutar el programa y la ubicación de los archivos. Como se observa en la figura, debemos estar situados arriba del directorio flujos pues así se llama el paquete. La instrucción sería la siguiente: C:\.....\>javac flujos\CopiaByt flujos\CopiaBytes.java es.java (para compilar) C:\...\>java flujos.CopiaByt flujos.CopiaBytes es (para ejecutar el archivo)
El archivo entonces debe estar situado en el mismo directorio en el que está situado el directorio flujos.
Flujos de Caracteres.
Algo muy similar podemos hacer para leer caracteres en lugar de bytes. Los flujos que leen caracteres heredan de la clase Reader y de la clase Writer las cuales también se encuentran en el paquete java.io. El siguiente ejemplo de código muestra el contenido de un archivo de texto en la consola. El funcionamiento del programa es prácticamente igual al que acabamos de mostrar, excepto porque en este caso, se leen caracteres en lugar de bytes y además la salida no se graba en otro archivo sino que se muestra en la consola. El lenguaje Java utiliza una codificación para los caracteres llamada “Unicode”. Ésta codificación utiliza dos bytes por po r cada caracter. La figura 5.26 muestra el código del programa.
Figura 5.24 Colocación de los archivos y programas
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
151
Figura 5.25 Código de la clase CopiaBytes que copia un archivo byte por byte.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
152
Figura 5.26 Código de la clase CopiaCaracteres La principal diferencia entre los códigos de las figuras 5.25 y 5.26 está en los flujos que utilizan pues uno está orientado a bytes y el otro a caracteres. El finally de la línea 19 nos asegura que siempre se cerrará el flujo sin importar lo que pase con el programa. En ocasiones cuando un programa se termina sin haber cerrado el flujo puede ocasionar que el archivo quede dañado o incompleto. Por eso es necesario cerrar los flujos antes de terminar los programas. En la línea 16 de la figura 5.26 observamos que el caracter leído se tiene convertir de entero a caracter. Este tipo de conversión recibe el nombre de “cast” y es muy utilizada para convertir datos primitivos o también objetos como vimos en el capítulo anterior. Si no realizáramos el “cast” el programa mostraría sólo una serie de números enteros que corresponderían a los códigos numéricos de cada carácter. Se deja de ejercicio al lector verificar lo anterior.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
153
Otros tipos de Flujos.
Existe una gran cantidad de flujos, cada uno tiene sus características particulares. Existen flujos para leer bytes, caracteres, líneas, elementos de arreglos, etc. Cerraremos esta discusión de flujos con un ejemplo de un flujo que lee líneas completas. En este caso, cada instrucción de lectura (readline()) lee toda una secuencia de caracteres hasta encontrar un caracter especial que es el fin de línea. Este flujo lo utilizaremos más adelante cuando veamos el ejemplo completo del programa de nómina. La figura 5.27 muestra el código de la clase CopiaLineas, la cual utiliza el flujo BufferedReader. Este flujo, aparte de leer toda la línea en una sola instrucción, lo hace de una manera más más eficiente pues utiliza un “buffer”. El buffer es un área de memoria en la cual se almacena temporalmente la información que se está leyendo o escribiendo. La ventaja de usar un buffer es que el sistema operativo de la computadora realiza una sola instrucción de lectura que llena el buffer, en lugar de realizar varias operaciones de lectura. Las operaciones de lectura que realiza el programa leen información del buffer. Cuando el programa ha consumido toda la información del buffer, se realiza otra operación de lectura y el buffer se vuelve a llenar. La escritura de datos se realiza de manera muy similar. Esto que los programas sean más eficientes en general. Serialización de Objetos
Los ejemplos de flujos de datos asociados a archivos que hemos visto hasta ahora pueden leer y escribir bytes, caracteres y líneas. Sin embargo, nosotros hemos trabajado extensivamente con objetos. Una pregunta que podemos hacernos es ¿Cómo podemos grabar y/o leer objetos en archivos? Existen básicamente dos posibilidades. La primera es grabar en archivos toda la información de los objetos convertida a bytes, caracteres o cadenas. Esto implicaría obtener los valores de los atributos de los objetos y grabarlos en forma de texto o binario (bytes). Posteriormente tendríamos que leer los archivos y crear de nueva cuenta los objetos con los valores leídos. Ésta opción tiene un gran inconveniente. Si un objeto hace referencia a otro objeto, no existe forma de guardar esa referencia en un archivo de manera sencilla o automática. La segunda alternativa es utilizar un mecanismo provisto por el lenguaje Java llamado Serialización de Objetos. Este mecanismo permite que un objeto sea grabado en disco a través de una secuencia de bytes que contiene contiene no sólo la información del objeto sino también de los objetos objetos a los cuales este hace referencia. Esto quedará más claro con los ejemplos que presentaremos más adelante.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
154
Figura 5.27 Código de la clase CopiaLineas La figura 5.28 muestra el proceso de serialización y de-serialización de objetos. Como se muestra en la figura, el proceso para escribir un objeto en disco disco consiste en convertir los objetos objetos a bytes y grabarlos en disco a través del método writeObject(Object). El proceso inverso consiste en leer los bytes del disco y crear los objetos a través del método readObject(). Para poder serializar un objeto, la clase a la cual pertenece debe implementar la interface Serializable. Esta interface en realidad no posee métodos pero se debe implementar para poder realizar el proceso de serialización. Los flujos que usaremos son el ObjectOutputStream y el ObjectInputStream los cuales tienen sendos métodos para grabar y leer objetos en archivos. La figura 5.29 muestra un ejemplo de código que crea dos objetos y los almacena en disco. La figura 5.30 muestra un ejemplo de código que lee objetos almacenados en disco y despliega su información. Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
155
Figura 5.28 Serialización de Objetos
Figura 5.29 Código de la clase GrabaObjetos que utiliza serialización para almacenar objetos en un archivo
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
156
El código de la figura 5.29 utiliza dos flujos, ambos de salida, pues sólo graba información. La clase FileOutputStream es un flujo que permite grabar bytes en un archivo y la clase ObjectOutputStream nos permite grabar objetos. Debido a que los objetos se almacenan en bytes, el objeto de la clase ObjectOutputStream que graba los objetos de la clase Empleado requiere a su vez de otro objeto de la clase FileOutputStream que grabe bytes, es por esa razón que en la línea 11, cuando se construye el objeto de la clase ObjectOutputStream se envía como parámetro del constructor al objeto de la clase FileOutputStream que se acaba de crear. Las líneas 15 y 16 graban los objetos en el disco y la línea 17 tiene como finalidad asegurarse que no quede nada pendiente de grabar en el buffer. Note que el catch de la línea 19 “atrapa” cualquier excepción. Esto nos permite no tener que poner varios catch’s uno por cada excepción posible, pero no es una buena práctica de programación hacerlo porque no podemos manejar la excepción mas adecuadamente. En general, siempre es mejor tratar cada posible excepción de manera independiente como lo habíamos hecho anteriormente. Pero quisimos mostrar que también se puede manejar un solo catch para todas las posibles excepciones. El código de la figura 5.29 no produce resultados pero al ejecutarse se crea el archivo “objetos.dat” el cual contiene toda la información de los de los objetos que se serializaron.
Figura 5.30 Código de la clase LeeObjetos que lee objetos que fueron serializados
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
157
Al ejecutar el código de la figura 5.30 se leen los objetos que habían sido guardados en el archivo “objetos.dat”. Observe que los flujos de flujos de entrada que se utilizan corresponden a los flujos de salida que se utilizaron en al código de la figura 5.29. La clase Empleado no debe haber sufrido ningún cambio, de lo contrario se marcará un error al momento de tratar de leer los objetos. La líneas 13 y 14 son las que leen la información y crean los objetos. Observe que se utiliza un “cast” para convertir el objeto leído a un objeto de la clase Empleado. El método readObject() regresa un objeto de la clase Object. En este código, podríamos haber evitado el uso del “cast” si hubiéramos declarado las variables objeto1 y objeto2 de la clase Object. Pero generalmente es mejor trabajar con las clases más específicas de los objetos. De hecho, como todas las clases extienden a la clase Object, ¡Cualquier objeto de cualquier clase puede estar almacenado en una variable de la clase Object! Algo interesante que debemos observar del código de la figura 5.30 es que en las líneas 16 y 17 se imprimen los objetos en la consola. Esto sólo tiene sentido si se ha implementado el método toString() en la clase Empleado, de lo contrario sólo mostrará la dirección de la memoria heap en la que está el objeto y no sus datos. La figura 5.31 muestra el código de la clase Empleado cuyos objetos pueden ser serializados y almacenados en disco. Observe que la clase implementa la interface Serializable. Esta implementación es necesaria para llevar a cabo la serialización. Sin embargo, la interface Serializable no contiene ningún método. Se utiliza sólo para indicar que los objetos de esa clase pueden ser serializados y almacenados en disco.
Figura 5.31 Código de la clase Empleado cuyos objetos pueden ser serializados
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
158
Serializando objetos que hacen referencia a otros objetos.
Los ejemplos mostrados anteriormente permiten grabar y leer objetos de la clase Empleado. Esta clase es muy sencilla y no contiene referencias a otros objetos. Sin embargo, es muy común que existan situaciones en las cuales un objeto hace referencia a otro o a varios objetos. Supongamos por ejemplo, que cada empleado está asignado a un departamento. Si representamos a los departamentos como objetos de la clase Departamento, entonces la clase Empleado hace referencia a la clase Departamento como se muestra en las figuras 5.32 y 5.33.
Figura 5.32 Código de la clase Empleado que hace referencia a la clase Departamento Como se muestra en la figura 5.33, si una clase serializable hace referencia a otras, todas las clases deben ser serializables. En este caso en particular, la clase Empleado hace referencia a la clase Departamento. Entonces ambas deben implementar la interface Serializable. De lo contario, cuando se quiera grabar en el disco el objeto, se generará la excepción: java.io.NotSerializableException. Esta excepción ocurre porque se intenta serializar TODA la información del objeto pero dentro de la información del objeto va también la información de los otros objetos a los cuales se hace referencia. En este caso, cuando se graba un objeto de la clase Empleado , se debe grabar la información del departamento al cual está asignado el empleado.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
159
Figura 5.33 Código de la clase Departamento
Existe sin embargo, una excepción a esta regla que consiste en utilizar la palabra transient en la declaración del atributo que no se desea grabar. En este caso y sólo en este caso, no es necesario que la clase de este atributo implemente la interface Serializable. Sin embargo, debe quedar muy claro que la información del atributo transient se perderá cuando se elimine el objeto de la memoria heap. Para hacer esta prueba, el lector debe modificar la línea 10 de la figura 5.32, escribiendo: transient Departamento departamento;
Con esta modificación ya no es necesario que la clase Departamento implemente la interface Serializable. Sin embargo, cuando se ejecute la clase LeeObjetos, se generará una excepción pues ahora el atributo departamento está vacio, es decir, su valor es nulo y por lo tanto no es posible ejecutar el método getNombre() que se utiliza en la línea 22 de la figura 5.32. El Ejemplo de la Nomina Revisitado…Revisitado Revisitado…Revisitado
Revisaremos ahora el ejemplo de la Nomina pero ahora agregaremos que los objetos sean leídos del disco. Como vimos anteriormente, tenemos dos alternativas, utilizar uti lizar archivos “normales” para guardar los atributos de los objetos o utilizar serialización de objetos. Para ilustrar ambas alternativas utilizaremos dos clases diferentes que efectúan la misma función. Debido a que hemos encapsulado el manejo de datos en la clase BaseDatos, todos los cambios que realizaremos serán en esta clase.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
160
Utilizando Archivos de Texto
Modificaremos la clase BaseDatos para que lea un archivo de texto y por cada línea del archivo se cree un objeto con los valores de de los atributos escritos en el archivo. El archivo de texto se muestra en la figura 5.34.
Figura 5.34 Archivo Trabajador.txt El archivo mostrado en la figura 5.34 se utiliza para almacenar los datos de entrada del programa de nómina. Cada línea del archivo “Trabajador.txt” corresponde a un trabajador. Los datos est án separados por comas. Recordemos que tenemos dos tipos de trabajadores: obreros y supervisores. supervisores. El primer dato entonces es el tipo. Una “S” indica que el trabajador es un supervisor y una “O” indica que es un obrero. El segundo dato es el nombre, el tercer dato es el salario por hora. El tercer dato puede ser las horas trabajadas o el premio (si se trata de un obrero) o el descuento (si se trata de un supervisor). Recordemos que a los o breros se les otorga un premio y a los supervisores un descuento si es que solicitaron un préstamo. En el caso de que se especifiquen las horas trabajadas, entonces la línea del archivo contendrá 5 datos. En caso contrario, la línea sólo contendrá 4 datos. En el caso de que no se especifiquen las horas trabajadas, se asumirá que el trabajador laboró 40 horas. Por ejemplo, la primera línea del archivo de la figura 5.34 corresponde a un obrero, su salario por hora es de 60 pesos, trabajó 40 horas y tiene un premio de 100 pesos. La segunda línea también corresponde a un obrero pero este laboró 35 horas. A continuación veremos el código que permite leer el archivo y cargar loos datos en el arreglo de trabajadores. Las figuras 5.35 y 5.36 muestran el código de la clase BaseDatos que lee la información de los trabajadores de un archivo de texto el cual puede ser creado con el block de notas o cualquier otro programa editor de textos. El código de ambas figuras se debe escribir en un solo archivo. Observe que los números de las líneas de la figura 5.36 continúan a los de la figura 5.35.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
161
Observamos que en la línea 6 de la figura 5.35 se importa la clase java.util.ArrayList. Esta clase nos permite definir arreglos de cualquier tipo de objeto y tiene la característica de crecer dinámicamente dinámicamente. Esto significa que no tenemos que saber el tamaño (o sea la cantidad de elementos que contendrá el arreglo) al momento de su creación. La línea 16 de la misma figura crea el arreglo de trabajadores. Observe que en la declaración del arreglo se utilizan los paréntesis angulares o “cuñas” para definir el tipo de los objetos que contendrá el arreglo. Esta característica de Java se llama “tipos genéricos” o “tipos parametrizados” aunque su estudio está fuera de los alcances de este libro, podemos simplemente decir que nos permiten definir los tipos de datos que tendrán los arreglos u otro ot ro tipo de colecciones de datos. La lógica del código de la figura 5.35 es básicamente la misma de los códigos que vimos anteriormente que leen flujos de datos. Esto es, se lee línea por línea hasta que se alcanza el fin del archivo. La línea 23 llama al método estático creaTrabajador(String)el cual recibe el String que contiene la información del trabajador y devuelve un objeto de la clase Trabajador (aunque en realidad es un Obrero o un Supervisor, ¡Recordemos el polimorfismo!). El método creaTrabajador(String) está definido en la figura 5.36. Observemos que en la línea 35 se llama al método split(String) de la clase String. Este método separa la línea en varios Strings el argumento que se envía a este método es el separador. En este caso le enviamos la coma (“,”) porque queremos que tome la coma como separador de los datos. El método split(String)toma el separador y regresa a su vez un arreglo de Strings. Cada uno de estos Strings corresponde a un dato del objeto. El arreglo de Strings se guarda en el arreglo datos[]. Las líneas 43 y 44 de la figura 5.36 5.36 convierten los datos datos de tipo String a datos de tipo int. El método trim() de la clase String asegura que no existan espacios en los Strings que se quieren convertir a números pues de lo contrario, se generaría una excepción. Algo adicional que observamos del código de la figura 5.36 es que dependiendo de la cantidad de datos de cada línea del archivo y del tipo de trabajador, se hacen diferentes llamados a los constructores de las clase Obrero y Supervisor. Debido a que hemos utilizado un objeto de la clase ArrayList para almacenar a los trabajadores, estamos obligados a realizar unos pequeños ajustes al código de la clase Main. La figura 5.37 muestra el código de la clase Main que utiliza un ArrayList en lugar de un arreglo simple de trabajadores.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
162
Figura 5.35 Código (Parcial) de la clase BaseDatos que lee los objetos desde un archivo de texto Ninguna de las demás clases: Impresora, Obrero, Supervisor y Trabajador sufre cambio alguno.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
163
Figura 5.36 Código (Continuación) de la clase BaseDatos que lee los objetos o bjetos desde un archivo de texto La línea 4 del código de la figura 5.37 importa la clase archivosTexto.BaseDatos. Observe que se refiere al código de la figura 5.35 pues en este paquete hemos puesto la clase BaseDatos. De esta forma, podemos tener clases que se llamen igual en un programa… siempre siempre y cuando no estén situadas en el mismo paquete. También es importante observar que el arreglo de trabajadores está almacenado en un ArrayList y que este objeto lo regresa precisamente el método cargaDatos() de la clase BaseDatos. Finalmente, vemos que también se ha cambiado el ciclo for de la línea 12. Este tipo de ciclo for se le llama “ciclo for for mejorado” y fue introducido introducido a partir de la versión 1.5 de Java. El funcionamiento es muy simple: se declara una variable, en este caso la variable t, la cual debe ser del mismo tipo de los elementos del arreglo, en este caso de tipo Trabajador. Las instrucciones dentro del ciclo for (en este caso la línea 13) se ejecutan tantas veces como Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
164
elementos existan en el arreglo. En cada iteración, la variable declarada en el for cambia al siguiente elemento del arreglo.
Figura 5.37 Código de la clase Main que utiliza un ArrayList de trabajadores Utilizando Serialización de Objetos
Finalmente, mostraremos la segunda alternativa para almacenar los datos de los trabajadores que consiste en almacenar los datos utilizando serialización de objetos. Para ello, modificaremos de nuevo la clase BaseDatos de tal forma que se lean los datos de un archivo que contenga los objetos serializados. Para lograr la serialización, tendremos que modificar las clases Trabajador, Obrero y Empleado pues ahora deben implementar la interface Serializable. La clase Main queda igual, excepto excepto porque ahora importaremos importaremos la clase clase BaseDatos de un paquete diferente. Las figuras 5.38 a 5.42 muestran el código de las clases modificadas para lograr la serialización de objetos. Como puede verse en la figura 5.38, la clase BaseDatos utiliza ahora la serialización de objetos para leer y cargar la información de los trabajadores en el arreglo. También se tuvo que modificar la clase Trabajador agregando el método estático setNumTrabajadores que se llama en la línea 21. Esto es debido a que cuando se leen los objetos con el método readObject() no se ejecutan los constructores y era precisamente ahí en donde se contaba el número de trabajadores. Ver línea 14 de la figura 4.51. Algo interesante del código de la figura 5.38 es que todos los objetos se leen con una sola instrucción de lectura, la cual está escrita en la línea 15. Esto puede lograrse porque la clase Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
165
ArrayList implementa la interface Serializable y entonces es posible almacenar todos los
objetos que contenga el arreglo en una sola instrucción de escritura como veremos más adelante.
Figura 5.38 Código de la clase BaseDatos que utiliza serialización de objetos
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
166
Figura 5.39 Código de la clase Obrero que implementa la interface Serializable
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
167
Figura 5.40 Código de la clase Trabajador implementando la interface Serializable Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
168
Figura 5.41 Código de la clase Supervisor implementando la interface Serializable Finalmente, mostramos el código de la clase GrabaEmpleados en la figura 5.42 el cual almacena el arreglo de trabajadores en un archivo. El funcionamiento de la clase GrabaEmpleados es muy simple. Se crea un arreglo de trabajadores utilizando la clase ArrayList. Este arreglo se serializa y se graba gr aba en el archivo “Empleados.dat”. Algo interesante que debemos mencionar es que podríamos haber grabado objeto por objeto. Esto es, podríamos haber escrito un ciclo que recorriera todo el arreglo de trabajadores y que fuera grabando trabajador por trabajador. Sin embargo, esto complicaría el código y además también haría más compleja la clase BaseDatos pues también tendría que leer trabajador por trabajador. Hemos aprovechado el hecho de que la clase ArrayList implementa la interface Serializable para que en una sola instrucción se grabe la información de todos los trabajadores en una sola instrucción, la cual está escrita en la línea 27 de la figura 5.42. Aunque es mucho más simple trabajar con serialización de objetos, como lo muestran los códigos anteriores, los programas que trabajan con muchos datos utilizan utilizan más frecuentemente archivos o programas especializados que manejan información que se llaman “Manejadores “Manejadores de Bases de Datos”. Éstos últimos tienen la ventaja de que traen incorporadas muchas funciones para el manejo de la información. Por ejemplo, se puede agregar, eliminar y modificar información de una manera muy sencilla. Además manejan índices para buscar y procesar información de una forma eficiente y rápida. Permiten también definir seguridad de los datos para que éstos no puedan ser Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
169
alterados por personas sin los permisos correspondientes y muchas otras funciones relativas al manejo de datos. Lo lenguajes de programación pueden comunicarse con estos programas de una manera muy sencilla.
Figura 5.42 Código de la clase GrabaEmpleados Resumen del Capítulo
En este capítulo vimos el manejo de excepciones y de flujos avanzados de datos. Las excepciones son situaciones extraordinarias que pueden ocurrir durante la ejecución de los métodos de los objetos o clases. Existen excepciones predefinidas en el lenguaje Java pero también es posible
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
170
crear nuevas excepciones y utilizarlas en los programas. Vimos que cuando un método hace un llamado a otro método que puede provocar provocar una excepción, es necesario necesario “atrapar” la excepción mediante bloques “ try-catch” o declarar que el método llamador “aventará” a su vez la excepción a quien lo llamó utilizando la palabra throws en su declaración. Finalmente, vimos el uso de flujos avanzados de datos. Los flujos son fuentes o destinos de datos y generalmente están asociados a dispositivos de entrada salida. Vimos que existen flujos que graban y leen archivos en bytes, caracteres o líneas completas. Podemos utilizar flujos para grabar y leer información de archivos almacenados en el disco de la computadora. También podemos utilizar flujos para leer información del teclado, enviar información a la consola, enviar y recibir información de otra computadora conectada a una red, etc. Utilizamos archivos para almacenar información de objetos y también utilizamos la serialización de objetos para almacenar los objetos en el disco. A esto se le llama “persistencia de objetos”. Para lograr la serialización, las clases deben implementar la interface Serializable. Esta interface no tiene métodos que deban implementarse pero es requisito implementarla. Otra cosa importante que debemos recordar es que si una clase hace referencia a otra, es un requisito que ambas implementen la interface Serializable de lo contrario, obtendremos una excepción. Si utilizamos la palabra transient podremos eliminar este requisito pero entonces los objetos referenciados se pierden y se asigna un valor de nulo a estos atributos.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
171
Ejercicios y Preguntas.
1. En el lenguaje Java se manejan excepciones (Exceptions) y errores (Errors). Describa la diferencia entre ambos conceptos. 2. Escriba la clase EnterosPositivos la cual tiene los métodos estáticos: suma(int,int), resta(int,int) y divide(int,int). Estas operaciones realizan la suma, la resta y la división de los enteros que reciben. Pueden recibir cualquier par de argumentos enteros pero el resultado debe ser un entero positivo menor o igual a 100, de lo contrario se generarán las siguientes excepciones: NumeroMuyGrandeException, NumeroNegativoException y NumeroDecimalEx NumeroDecimalException ception. Pruebe su código con el siguiente código: public class EnterosPositivos { public static void main(String[] args) { try {suma(99,2); {suma(99,2); } catch (Exception ex) {ex.printStackTrace();} {ex.printStackTrace();} try {resta (3,4); } catch (Exception ex) {ex.printStackTrace();} try {divide(10,3);} catch (Exception ex) {ex.printStackTrace();} } // Definir las funciones y sus excepciones }
El resultado deberá similar siguiente: ejemplo.NumeroMuyGrandeException at ejemplo.EnterosPositivos.checa(EnterosPositivos.java:34) at ejemplo.EnterosPositivos.suma(EnterosPositivos.java:13) at ejemplo.EnterosPositivos.main(EnterosPositivos.java:7) ejemplo.NumeroNegativoException at ejemplo.EnterosPositivos.resta(EnterosPositivos.java:18) at ejemplo.EnterosPositivos.main(EnterosPositivos.java:8) ejemplo.NumeroDecimalException at ejemplo.EnterosPositivos.divide(EnterosPositivos.java:26) at ejemplo.EnterosPositivos.main(EnterosPositivos.java:9)
3. Escriba código que provoque cada una de las siguientes excepciones : ArrayIndexOutOfBoundsException y NullPointerException.
ClassCastException,
4. Modifique código del ejemplo de la figura 5.3 de tal forma que el método método compraCoca() EnvaseRotoException pueda aventar también las excepciones y NoTieneGasException. Modifique el código de la figura 5.4 para que maneje estas excepciones adicionales. 5. Describa la diferencia entre el bloque try y el bloque finally. En qué casos es recomendable usar el bloque finally. Escriba un ejemplo de código que lo utilice adecuadamente.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
172
6. Explique la razón por la cual algunas excepciones deben ser “atrapadas” con el bloque catch y otras no. ¿Cómo podría un programador crear excepciones que no tengan que ser “atrapadas”? 7. EL siguiente código maneja las excepciones de manera incorrecta:
Explique la razón por la cual las excepciones no son manejadas correctamente y modifique el código de tal manera que las excepciones se manejen de manera correcta. Puede utilizar comentarios que simulen acciones correctivas en los bloques catch. 8. El siguiente código marca errores al compilar. Explique por qué marca error y corríjalo son agregar bloques try-catch.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
173
9. Describa que excepciones pueden suceder cuando se intenta abrir un flujo de datos asociado a un archivo. 10. Escriba un programa que lea un archivo de texto con información sobre la temperatura en varias ciudades de México. Un ejemplo del archivo es el siguiente: Entrada.txt: Guadalajara, 28 Monterrey, 25 Veracruz, 35 …
El programa deberá producir un resultado similar al siguiente: Número de Ciudades: 20 Temperatura más alta: Veracruz, 35 Temperatura más baja: Monterrey, 25 Promedio de Temperaturas: 30
11. EL siguiente código almacena de manera permanente la información de los trabajadores del ejemplo de la nómina presentado en este capítulo. package serializacion; import import import import
java.io.FileOutputStream; java.io.ObjectOutputStream; java.util.ArrayList; nomina.*;
public class GrabaTrabajadores { public static void main(String args[]) { try { FileOutputStream fos = new FileOutputStream("Empleados.dat"); ObjectOutputStream oos = new ObjectOutputStream(fos); ArrayList trabajadores = new ArrayList(); trabajadores.add(new Obrero("Armando Paredes", 60, 100)); trabajadores.add(new Obrero("Cindy Nero", 50, 35, 50)); trabajadores.add(new Obrero("Alan Brito", 40, 48, 0)); trabajadores.add(new Obrero("Marco López", 55, 0)); trabajadores.add(new Supervisor("Rosario Márquez", 180, 150)); trabajadores.add(new Obrero("Sergio Martínez", 75, 38, 120)); trabajadores.add(new Obrero("Luisa Domínguez", 50, 30, 110)); trabajadores.add(new Obrero("Petra Barrera", 55, 0)); trabajadores.add(new Obrero("Manuel Flores", 65, 35, 0)); trabajadores.add(new Supervisor("Alma Rios", 175, 0)); for (Trabajador t : trabajadores) {
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
174
oos.writeObject(t); oos.flush(); } oos.close(); } catch (Exception e) { e.printStackTrace(); } } }
¿Cuál es la diferencia entre este código y el de la figura 5.42? Modifique el código de la clase BaseDatos de la figura 5.38 para que lea la información almacenada utilizando este código. 12. Escriba un programa en Java que lea un archivo de texto y muestre la cantidad de cada una de las vocales que tiene el archivo. Por ejemplo para el archivo: Hola ¡La palabra murciélago tiene todas las vocales! La salida deberá ser la siguiente: Número de a’s: 9 Número de e’s: 4
Número de i’s: 2 Número de o’s: 4 Número de u’s: 1
13. Dado el siguiente diagrama de clases, escriba código que permita crear varios objetos de la clase Computadora y almacenarlos en un archivo utilizando serialización de objetos.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
175
Observe que un objeto de la clase Computadora está compuesto a su vez por objetos de las clases Memoria, Monitor y DiscoDuro. Escriba también un programa que lea el archivo y despliegue la información de los objetos. La salida del programa deberá ser la siguiente: *** Características de la Computadora: HP dv2125 *** Monitor --> Marca: Samsung, Tamaño: 21 pulgadas Disco Duro --> Marca: Seagate, Capacidad: 1000 gigas Memoria --> Marca: ScanDisk, Capacidad: 512 megas *** Características de la Computadora: LG xl123 *** Monitor --> Marca: LG, Tamaño: 19 pulgadas Disco Duro --> Marca: Maxtor, Capacidad: 500 gigas Memoria --> Marca: Viking, Capacidad: 1024 megas
14. Describa las ventajas y desventajas que tiene el uso de la serialización para lograr la persistencia de objetos en lugar de utilizar archivos de texto o de bytes.
15. Modifique el código de la clase GrabaEmpleados de la figura figura 5.42 de tal forma forma que en lugar de asignar los datos de los trabajadores en el código, éstos sean leídos desde el teclado. Sugerencia: utilice el flujo de entrada System.in, el cual es un objeto de la clase InputStream y está asociado al teclado de la computadora.
Conceptos de Programación Orientada a Objetos
Héctor A. Andrade G.
176