Download Apuntes Fundamentos Java
Document related concepts
no text concepts found
Transcript
Capítulo 1 ObjetosyClases Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 1 ObjetosyClases 1.1 Objetos y Clases Clase: Una clase es una plantilla donde vamos a definir unos atributos y unos métodos. Una clase es la implementación de un tipo de objeto, considerando los objetos como instancias de las clases. Objeto: Cuando se crea un objeto, se instancia una clase, mediante el operador new. Se ha de especificar de qué clase es el objeto instanciado, para que el compilador comprenda las características del objeto. 1.2 Creación de Objetos Cuando se instancia un objeto el compilador crea en la memoria dinámica un espacio para tantas variables como atributos tenga la clase a la que pertenece el objeto. 1.3 Invocación de métodos Podemos comunicarnos con los objetos invocando sus métodos. Generalmente, los objetos hacen algo cuando invocamos un método. 1.4 Parámetros Los métodos pueden tener parámetros para aportar información adicional para realizar una tarea. El encabezado de un método se denomina signatura, proporciona la información necesaria para invocar dicho método. Los métodos pueden tener cualquier número de parámetro. 1.5 Tipos de dato Los parámetros tienen tipos. El tipo define la clase de valor que un parámetro puede tomar. 1.6 Instancias múltiples Se pueden crear muchos objetos similares a partir de una sola clase. Cada uno de los atributos del objeto tendrá sus propios valores. 1.7 Estado Los objetos tienen un estado. El estado está representado por todos los valores almacenados en los campos o atributos. 1.8 ¿Qué es lo que contiene un objeto? Cuando se crea un objeto, se instancia una clase, mediante el operador new. Todos los objetos de la misma clase tienen los mismos campos. Los valores concretos de cada campo particular de cada objeto pueden ser diferentes. Los métodos se definen en la clase del objeto. Todos los objetos de la misma clase tienen los mismos métodos. 2 1.9 Código Java Cuando programamos en Java, escribimos instrucciones para invocar métodos sobre objetos. Tenemos que escribir los comandos correspondientes de forma textual. Cuando creamos un objeto, lo que hacemos es almacenar ese objeto en una variable. Para llamar a un método, escribimos el nombre del objeto seguido de punto y seguido del nombre del método, terminamos con una lista de parámetros o con un par de paréntesis vacíos si no hay parámetros. Todas las instrucciones Java terminan con un punto y coma. 1.10 Interacción entre Objetos Lo normal es crear una clase principal que inicie el resto. 1.11 Código Fuente Cada clase tiene algún código fuente asociado. El código fuente es un texto que define los detalles de la clase. El código fuente es un texto escrito en lenguaje de programación Java y define qué campos y métodos tiene la clase y qué ocurre cuando se invoca un método. El arte de la programación, que no es tarea fácil, consiste en aprender cómo escribir estas definiciones de clases. Cuando realiza algún cambio en el código, la clase necesita ser compilada haciendo clic en el botón Compile. Una vez que una clase ha sido compilada, se pueden crear nuevamente objetos y probar sus cambios. 1.13 Valores de retorno Los métodos que devuelven o retornan valores nos permiten obtener información sobre un objeto mediante una llamada al método. Quiere decir que podemos usar métodos tanto para cambiar el estado de un objeto como para investigar su estado. Con la palabra void indicamos que ese método no retoma ningún resultado. 1.14 Objetos como parámetros Los objetos pueden ser pasados como parámetros a los métodos de otros objetos. En el caso de que un método espere un objeto como parámetro, el nombre de la clase del objeto que espera se especifica como el tipo de parámetro en la signatura de dicho método. 3 1 Objetos y Clases ............................................................................................................................... 2 1.1 Objetos y Clases ........................................................................................................................ 2 1.2 Creación de Objetos ................................................................................................................... 2 1.3 Invocación de métodos .............................................................................................................. 2 1.4 Parámetros ................................................................................................................................. 2 1.5 Tipos de dato.............................................................................................................................. 2 1.6 Instancias múltiples ................................................................................................................... 2 1.7 Estado ........................................................................................................................................ 2 1.8 ¿Qué es lo que contiene un objeto? ........................................................................................... 2 1.9 Código Java ............................................................................................................................... 3 1.10 Interacción entre Objetos ........................................................................................................... 3 1.11 Código Fuente ............................................................................................................................ 3 1.13 Valores de retorno...................................................................................................................... 3 1.14 Objetos como parámetros .......................................................................................................... 3 4 Capítulo 2 De fin ici ones deClases Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 2 DefinicionesdeClases 2.3 La cabecera de la clase El código de las clases puede dividirse en dos partes principales: un envoltorio exterior que simplemente da nombre a la clase y una parte interna mucho más grande que hace todo el trabajo. El envoltorio exterior tiene la siguiente apariencia: public class <NombreClase> { <parte interna> } El envoltorio exterior de las diferentes clases es muy parecida, su principal finalidad es proporcionar un nombre a la clase. Por convenio, los nombres de las clases comienzan siempre con una letra mayúscula. 2.3.1 Palabras clave o reservadas Las palabras reservadas son identificadores predefinidos que tienen un significado para el compilador y por tanto no pueden usarse como identificadores creados por el usuario en los programas. Las palabras reservadas en Java ordenadas alfabéticamente son las siguientes: abstract assert boolean break byte case catch char class const continue default do double else enum extends final finally float for goto if implements import instanceof int interface long native new package private protected public return short static strictfp super switch synchronized this throw throws transient try void volatile while 2.4 Campos, constructores y métodos La parte interna de la clase es el lugar en el que definimos los campos, constructores y métodos que dan a los objetos de la clase sus características particulares y su comportamiento. Podemos resumir las características esenciales de estos tres componentes de una clase como sigue: Los campos almacenan datos para que cada objeto los use. Los constructores permiten que cada objeto se prepare adecuadamente cuando es creado. Los métodos implementan el comportamiento de los objetos. En Java existen muy pocas reglas sobre el orden que se puede elegir para definir los campos, los constructores y los métodos dentro de una clase. Es importante elegir un estilo y luego usarlo de manera consistente, porque de este modo las clases serán más fáciles de leer y de comprender. 2 Orden de campos, constructores y métodos: public class NombreDeClase { Campos Constructores Métodos } 2.4.1 Campos Los campos almacenan datos de manera persistente dentro de un objeto y son accesibles para todos los métodos del objeto. Los campos también son conocidos como variables de instancia. Como los campos pueden almacenar valores que pueden variar a lo largo del tiempo, podemos llamarlos variables. Los campos son pequeñas cantidades de espacio dentro de un objeto que pueden usarse para almacenar datos de manera persistente. Todos los objetos una vez creados dispondrán de un espacio para cada campo declarado en su clase. Las variables de clase, también se conocen como variables estáticas, siempre tienen el mismo valor para todos los objetos de una determinada clase. En realidad no son variables sino constantes Los comentarios se insertan en el código de una clase para proporcionar explicaciones a los lectores humanos. No tienen ningún efecto sobre la funcionalidad de la clase. Los comentarios de una sola línea van precedidos de los caracteres “//”. Los comentarios más detallados, que frecuentemente ocupan varias líneas, se escriben generalmente en la forma de comentarios multilínea: comienzan con el par de caracteres “/*” y terminan con el par “*/”. Para definir una variable de campo dentro de una clase seguiremos el siguiente patrón: Normalmente, comienzan con la palabra clave private. Incluyen un nombre de tipo, int, String, etc. Incluyen un nombre elegido por el usuario para la variable de campo. Terminan en punto y coma. El tipo de un campo especifica la naturaleza del valor que puede almacenarse en dicho campo. Si el tipo es una clase, el campo puede contener objetos de esa clase. 2.4.2 Constructores Los constructores permiten que cada objeto sea preparado adecuadamente cuando es creado. Esta operación se denomina inicialización. El constructor inicializa el objeto en un estado razonable. Uno de los rasgos distintivos de los constructores es que tienen el mismo nombre que la clase en la que son definidos. El nombre del constructor sigue inmediatamente a la palabra public. Los campos del objeto se inicializan en el constructor, bien con valores fijos, o bien con parámetros del propio constructor. En Java todos los campos son inicializados automáticamente con un valor por defecto, si es que no están inicializados explícitamente. 3 El valor por defecto para los campos enteros es 0. Sin embargo, es preferible escribir explícitamente las asignaciones. No hay ninguna desventaja en hacer esto y sirve para documentar lo que está ocurriendo realmente. 2.5 Parámetros: recepción de datos La manera en que los constructores y los métodos reciben valores es mediante sus Parámetros. Los parámetros son otro tipo de variable, igual que los campos, por lo que se utilizan para almacenar datos. Los parámetros se definen en el encabezado de un constructor o un método. Los parámetros transportan datos que tienen su origen fuera del constructor o método y hacen que esos datos estén disponibles en el interior del constructor o método. Puesto que permiten almacenar valores, los parámetros formales constituyen otra clase de variables. Distinguimos entre nombres de los parámetros dentro de un constructor o un método y valores de los parámetros fuera de un constructor o un método. Hacemos referencia a los nombres como parámetros formales y a los valores como parámetros reales. Un parámetro formal está disponible para un objeto sólo dentro del cuerpo del constructor o del método que lo declara. Decimos que el alcance de un parámetro está restringido al cuerpo del constructor o del método en el que es declarado. El tiempo de vida de un parámetro se limita a una sola llamada de un constructor o método. Cuando se invoca un constructor o método, se crea el espacio adicional para las variables de parámetro y los valores externos se copia en dicho espacio. Una vez que completó su tarea, los parámetros formales desaparecen y se pierden los valores que contienen. Por el contrario, el tiempo de vida de un campo es el mismo tiempo de vida que el del objeto al que pertenece. 2.5.1 Elección de los nombres de variable Es conveniente elegir nombres que proporcionen algo de información al lector. 2.6 Asignación Destacamos la necesidad de almacenar el valor de corta vida de un parámetro dentro de algún lugar más permanente, una variable de campo. Las sentencias de asignación almacenan el valor representado por el lado derecho de la sentencia en una variable nombrada a la izquierda. Una regla sobre las sentencias de asignación es que el tipo de una expresión debe coincidir con el tipo de la variable a la que es asignada. La misma regla se aplica también entre los parámetros formales y los parámetros reales: el tipo de una expresión de un parámetro real debe coincidir con el tipo de una variable parámetro formal. 2.7 Métodos Los métodos se componen de dos partes: una cabecera y un cuerpo. Es importante distinguir entre la cabecera del método y declaración de campos porque son muy parecidos. Podemos decir que algo es un método y no un campo porque está seguido de un par de paréntesis: «(» y «)». Obsérvese también que no hay un punto y coma al final de la signatura. 4 El cuerpo del método es la parte restante del método, que aparece a continuación de la cabecera. Está siempre encerrado entre llaves: «{« y »}». Los cuerpos de los métodos contienen las declaraciones y las sentencias que definen qué ocurre dentro de un objeto cuando es invocado ese método. Las declaraciones se utilizan para crear espacio adicional de variables temporales, mientras que las instrucciones describen las acciones del método. Cualquier conjunto de declaraciones y sentencias, ubicado entre un par de llaves, es conocido como un bloque. Por lo que el cuerpo de una clase y los cuerpos de todos los métodos de las clases son bloques. Existen, dos diferencias significativas entre las cabeceras de los constructores de una clase y de los demás métodos: Por un lado los constructores tienen el mismo nombre que la clase en la que están definidos, y por otro los métodos siempre tienen un tipo de retorno (aunque sea void) mientras que el constructor no tiene tipo de retorno. El tipo de retorno se escribe exactamente antes del nombre del método. Es una regla de Java que el constructor no puede tener ningún tipo de retorno. Por otro lado, tanto los constructores como los métodos pueden tener cualquier número de parámetros formales, inclusive pueden no tener ninguno. Los métodos pueden tener una sentencia return y es la responsable de devolver un valor que coincida con el tipo de retorno de la signatura del método. Cuando un método contiene una sentencia return, siempre es la última sentencia que se ejecuta del mismo porque una vez que se ejecutó esta sentencia no se ejecutarán más sentencias en el método. Los tipos de retorno y las instrucciones de retorno funcionan conjuntamente. Podemos decir que una llamada a un método es una especie de pregunta que se la hace el objeto y el valor de retorno proporcionado por el método es la respuesta que el objeto da a esa pregunta. 2.8 Métodos Selectores y Mutadores Los métodos selectores “get” devuelven información sobre el estado del objeto. Proporcionan acceso a información acerca del estado del objeto. Un método selector contiene generalmente una sentencia return para devolver información de un valor en particular. Devolver un valor significa que se pasa una cierta información internamente entre dos partes diferentes del programa. A los métodos que modifican el estado de su objeto los llamamos métodos mutadores. Los métodos mutadores cambian el estado de un objeto. La forma básica de mutador admite un único parámetro y este valor se utiliza para sobreescribir directamente lo que haya almacenado en uno de los campos del objeto. Los métodos mutadores los denominamos métodos "set". La cabecera de un método de mutador, "set", normalmente tiene tipo de retorno void y un solo parámetro formal, el nuevo valor del campo a modificar. Un tipo de retorno void significa que el método no devuelve ningún valor cuando es llamado; es significativamente diferente de todos los otros tipos de retorno. En el cuerpo de un método void, esta diferencia se refleja en el hecho de que no hay ninguna sentencia return. Los métodos mutadores siempre tienen al menos una sentencia de asignación. El sumar (o restar) una cantidad al valor de una variable es algo tan común que existe un operador de asignación compuesto, especial para hacerlo: «+=». 5 2.9 Imprimir desde Métodos El método System.out.println(<parametro>) imprime su parámetro en la terminal de texto. Una sentencia como System.out.println("# Línea BlueJ"); imprime literalmente la cadena que aparece entre el par de comillas dobles. Todas estas sentencias de impresión son invocaciones al método println del objeto System.out que está construido dentro del lenguaje Java. Cuando se usa el símbolo «+» entre una cadena y cualquier otra cosa, este símbolo es un operador de concatenación de cadenas (es decir, concatena o reúne cadenas para crear una nueva cadena) en lugar de ser el operador aritmético de suma. El método println se puede llamar sin contener ningún parámetro de tipo cadena. Esto está permitido y el resultado de la llamada será dejar una línea en blanco entre esta salida y cualquier otra que le siga. 2.13 Tomas de decisión: la instrucción condicional Una sentencia condicional realiza una de dos acciones posibles basándose en el resultado de una prueba. También son conocidas como sentencias if. Se evalúa el resultado de una verificación o prueba, si el resultado es verdadero entonces hacemos una cosa, de lo contrario hacemos algo diferente. Una sentencia condicional tiene la forma general descrita en el siguiente pseudo-código: if (llevar a cabo alguna prueba que dé un resultado verdadero o falso){ Si la prueba dio resultado verdadero, ejecutar estas sentencias } else{ Si el resultado dio falso, ejecutar estas sentencias } La prueba que se usa en una sentencia condicional es un ejemplo de una expresión booleana. Las expresiones booleanas tienen sólo dos valores posibles: verdadero o falso (true o false). Se las encuentra comúnmente controlando la elección entre los dos caminos posibles de una sentencia condicional. 2.16 Variables locales Una variable local es una variable que se declara y se usa dentro de un solo método. Las declaraciones de las variables locales son muy similares a las declaraciones de los campos pero las palabras private o public nunca forman parte de ellas. Es muy común inicializar variables locales cuando se las declara. Se crean cuando se invoca un método y se destruyen cuando el método termina. Los constructores también pueden tener variables locales. Las variables locales se usan frecuentemente como lugares de almacenamiento temporal para ayudar a un método a completar su tarea. Podemos considerarlas como un almacenamiento de datos para un único método. Su alcance y tiempo de vida se limitan a los del método. Un error común es usar una variable local del mismo nombre que un campo evitará que el campo sea accedido dentro de un método. 6 2.17 Campos, parámetros y variables locales Las tres clases de variables pueden almacenar un valor acorde a su definición de tipo de dato. Los campos se definen fuera de los constructores y de los métodos. Los campos se usan para almacenar datos que persisten durante la vida del objeto, de esta manera mantienen el estado actual de un objeto. Tienen un tiempo de vida que finaliza cuando termina el objeto. El alcance de los campos es la clase: la accesibilidad de los campos se extiende a toda la clase y por este motivo pueden usarse dentro de cualquier constructor o método de clase en la que estén definidos. Como son definidos como privados (private), los campos no pueden ser accedidos desde el exterior de la clase. Los parámetros formales y las variables locales persisten solamente en el lapso durante el cual se ejecuta un constructor o un método. Su tiempo de vida es tan largo como una llamada, por lo que sus valores se pierden entre llamadas. Por este motivo, actúan como lugares de almacenamiento temporales antes que permanentes. Los parámetros formales se definen en el encabezado de un constructor o de un método. Reciben sus valores desde el exterior, se inicializan con los valores de los parámetros actuales que forman parte de la llamada al constructor o al método. Los parámetros formales tienen un alcance limitado a su definición de constructor o de método. Las variables locales se declaran dentro del cuerpo de un constructor o de un método. Pueden ser inicializadas y usadas solamente dentro del cuerpo de las definiciones de constructores o métodos. Las variables locales deben ser inicializadas antes de ser usadas en una expresión, no tienen un valor por defecto. Las variables locales tienen un alcance limitado al bloque en el que son declaradas. No son accesibles desde ningún lugar fuera de ese bloque. 7 Contenido 2 Definiciones de Clases ..................................................................................................................... 2 2.3 La cabecera de la clase .............................................................................................................. 2 2.3.1 2.4 Palabras clave o reservadas ................................................................................................ 2 Campos, constructores y métodos ............................................................................................. 2 2.4.1 Campos ............................................................................................................................... 3 2.4.2 Constructores ...................................................................................................................... 3 2.5 Parámetros: recepción de datos ................................................................................................. 4 2.5.1 Elección de los nombres de variable .................................................................................. 4 2.6 Asignación ................................................................................................................................. 4 2.7 Métodos ..................................................................................................................................... 4 2.8 Métodos Selectores y Mutadores ............................................................................................... 5 2.9 Imprimir desde Métodos ............................................................................................................ 6 2.13 Tomas de decisión: la instrucción condicional .......................................................................... 6 2.16 Variables locales ........................................................................................................................ 6 2.17 Campos, parámetros y variables locales .................................................................................... 7 8 Capítulo 3 Interacción deObjetos Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 3 InteraccióndeObjetos 3.2 Abstracción y Modularización Cuando un problema se agranda se vuelve más difícil mantener todos los detalles al mismo tiempo, aumenta la complejidad. La solución que usamos para tratar el problema de la complejidad es la abstracción. La abstracción es la habilidad de ignorar los detalles de las partes para centrar la atención en un nivel más alto de un problema. Dividimos el problema en subproblemas, luego en sub-subproblemas y así sucesivamente, hasta que los problemas resultan suficientemente fáciles de tratar. Una vez que resolvemos uno de los subproblemas no pensamos más sobre los detalles de esa parte, pero tratamos la solución hallada como un bloque de construcción para nuestro siguiente problema. Esta técnica se conoce como la técnica del divide y vencerás. La modularización es el proceso de dividir un todo en partes bien definidas que pueden ser construidas y examinadas separadamente, las cuales interactúan entre sí de maneras bien definidas. La modularización y la abstracción se complementan mutuamente. La modularización es el proceso de dividir cosas grandes (problemas) en partes más pequeñas. La abstracción es la habilidad de ignorar los detalles para concentrarse en el cuadro más grande. 3.3 Abstracción en el Software En el caso de programas complejos, para mantener una visión global del problema tratamos de identificar los componentes que podemos programar como entidades independientes, y luego intentamos utilizar esos componentes como si fueran partes simples sin tener en cuenta su complejidad interna. En programación orientada a objetos, estos componentes y subcomponentes son objetos. Las clases definen tipos. El nombre de una clase puede ser usado como el tipo de una variable. Las variables cuyo tipo es una clase pueden almacenar objetos de dicha clase. El tipo de un campo especifica la naturaleza del valor que puede almacenarse en dicho campo. Si el tipo es una clase, el campo puede contener objetos de esa clase. 3.6 Diagramas de Clases con Diagramas de Objetos El diagrama de clases muestra las clases de una aplicación y las relaciones entre ellas. Da información sobre el código. Representa la vista estática de un programa. El diagrama de objetos muestra los objetos y sus relaciones en un instante determinado de la ejecución de una aplicación. Da información sobre los objetos en tiempo de ejecución. Representa la vista dinámica de un programa. a) diagrama de objetos; b) diagrama de clases 2 El diagrama de objetos también muestra otro detalle importante, cuando una variable almacena un objeto, éste no es almacenado directamente en la variable sino que en la variable sólo se almacena una referencia al objeto. Referencia a un objeto. Las variables de tipo objeto almacenan referencias a los objetos. 3.7 Tipos Primitivos y Tipos Objeto Java reconoce dos clases de tipos muy diferentes: los tipos primitivos y los tipos objeto. Los tipos primitivos están todos predefinidos en el lenguaje Java Los tipos primitivos en Java son todos los tipos que no son objetos. Los tipos primitivos más comunes son los tipos int, booleano, char, double y long. Los tipos primitivos no poseen métodos. Tanto los tipos primitivos como los tipos objeto pueden ser usados como tipos, pero existen situaciones en las que se comportan de manera muy diferente. Una diferencia radica en cómo se almacenan los valores. Los valores primitivos se almacenan directamente en una variable. Por otro lado, los objetos no se almacenan directamente en una variable sino que se almacena una referencia al objeto. 3.8 El código fuente para ClockDisplay Ver código 3.3, página 71. Operadores Lógicos Los operadores lógicos operan con valores booleanos (verdadero o falso) y producen como resultado un nuevo valor booleano. Los tres operadores lógicos más importantes son «y», «o» y «no». En Java se escriben: && (y), || (o) y ! (no). La expresión a && b es verdadera si tanto a como b son verdaderas, en todos los otros casos es falsa. La expresión a || b es verdadera si alguna de las dos es verdadera, puede ser a o puede ser b o pueden ser las dos; si ambas son falsas el resultado es falso. La expresión !a es verdadera si a es falso, y es falsa si a es verdadera. 3.8.2 Concatenación de cadenas El operador suma (+) tiene diferentes significados dependiendo del tipo de sus operandos. Si ambos operandos son números, el operador + representa la adición. Si los operandos son cadenas, el significado del signo más es la concatenación de cadenas y el resultado es una única cadena compuesta por los dos operandos. Si uno de los operandos del operador más es una cadena y el otro no, el operando que no es cadena es convertido automáticamente en una cadena y luego se realiza la concatenación correspondiente. Esta conversión funciona para todos los tipos. Cualquier tipo que se «sume» con una cadena, automáticamente es convertido a una cadena y luego concatenado. 3.8.3 El Operador Módulo El operador módulo (%) calcula el resto de una división entera. (27%4) será 3. 3 3.9 Objetos que Crean Objetos Los objetos pueden crear otros objetos usando el operador new. La sintaxis de una operación para crear un objeto nuevo es: new NombreDeClase(lista-de-parámetros) La operación new hace dos cosas: Crea un nuevo objeto de la clase nombrada. Ejecuta el constructor de dicha clase. Si el constructor de la clase tiene parámetros, los parámetros actuales deben ser proporcionados en la sentencia new. 3.10 Constructores Múltiples Es común que las declaraciones de clases contengan versiones alternativas de constructores o métodos que proporcionan varias maneras de llevar a cabo una tarea en particular mediante diferentes conjuntos de parámetros. Este punto se conoce como sobrecarga de un constructor o método. Una clase puede contener más de un constructor o más de un método con el mismo nombre, siempre y cuando tengan distintos conjuntos de parámetros que se diferencien por sus tipos. 3.11 Llamadas a Métodos 3.11.1 Llamada a métodos internos Los métodos pueden llamar a otros métodos de la misma clase como parte de su implementación. Se denomina llamada a método interno. Se denomina así porque este método está ubicado en la misma clase en que se produce su llamada. Las llamadas a métodos internos tienen la siguiente sintaxis: nombreDelMétodo(lista-de-parámetros) Cuando se encuentra una llamada a un método, se ejecuta este último, y después de su ejecución se vuelve a la llamada al método y se continúa con la sentencia que sigue a la invocación. Para que la llamada a un método coincida con la signatura del mismo, deben coincidir tanto el nombre del método como su lista de parámetros. 3.11.2 Llamada a métodos externos Los métodos pueden llamar a métodos de otros objetos usando la notación de punto: se denomina llamada a método externo. La sintaxis de una llamada a un método externo es: objeto.nombreDelMétodo(lista-de-parámetros) Esta sintaxis se conoce con el nombre de “notación con punto”. Consiste en un nombre de objeto, un punto, el nombre del método y los parámetros para la llamada Es particularmente importante apreciar que usamos aquí el nombre de un objeto y no el nombre de una clase. El conjunto de métodos de un objeto que está disponible para otros objetos se denomina su interfaz. 4 3.12 Otro ejemplo de interacción de objetos 3.12.2 La Palabra Clave this Observe la siguiente sentencia con la palabra clave this: this.from = from; La línea en su totalidad es una sentencia de asignación, asigna el valor del lado derecho (from) a la variable que está del lado izquierdo (this.from) del símbolo igual (=). El motivo por el que se usa esta construcción radica en que tenemos una situación que se conoce como sobrecarga de nombres, y significa que el mismo nombre es usado por entidades diferentes. Es importante comprender que los campos de un objeto y los parámetros o variables locales son variables que existen independientemente unas de otras, aun cuando compartan nombres similares. Un parámetro o variable local y un campo que comparten un nombre no representan un problema para Java. La especificación de Java responde a esta pregunta: Java especifica que siempre se usará la declaración más cercana encerrada en un bloque. Dado que el parámetro o variable local from está declarado en el método y el campo from está declarado en la clase, se usará el parámetro pues su declaración es la más cercana a la sentencia que lo usa. Lo que necesitamos es un mecanismo para acceder a un campo cuando existe una variable con el mismo nombre declarada más cerca de la sentencia que la usa. Este mecanismo es justamente lo que significa la palabra clave this. La expresión this hace referencia al objeto actual. Al escribir this.from estamos haciendo referencia al campo del objeto actual, por lo que esta construcción nos ofrece una forma de referirnos a los campos en lugar de a los parámetros cuando tienen el mismo nombre. Ahora podemos leer la sentencia de asignación nuevamente: this.from = from; Como podemos ver, esta sentencia tiene el mismo efecto que la siguiente: campo de nombre "from" = parámetro de nombre "from"; Asigna el valor del parámetro from al campo del mismo nombre y por supuesto, esto es exactamente lo que necesitamos hacer para inicializar el objeto adecuadamente. La razón por lo que hacemos esto radica en la legibilidad del código. Si un nombre describe perfectamente la finalidad, resulta razonable usarlo como nombre de parámetro y de campo y eliminar los conflictos de nombres usando la palabra clave this en la asignación. 5 3.13 Uso del depurador, Debugger Un depurador es una herramienta de software que ayuda a examinar cómo se ejecuta una aplicación. Puede usarse para encontrar problemas. Un depurador es un programa que permite que los programadores ejecuten una aplicación paso a paso. Generalmente, ofrece funciones para detener y comenzar la ejecución de un programa en un punto seleccionado del código y para examinar los valores de las variables. 6 3 Interacción de Objetos ...................................................................................................................... 2 3.2 Abstracción y Modularización ................................................................................................... 2 3.3 Abstracción en el Software ........................................................................................................ 2 3.6 Diagramas de Clases con Diagramas de Objetos ...................................................................... 2 3.7 Tipos Primitivos y Tipos Objeto................................................................................................ 3 3.8 El código fuente para ClockDisplay .......................................................................................... 3 3.8.2 Concatenación de cadenas .................................................................................................. 3 3.8.3 El Operador Módulo ........................................................................................................... 3 3.9 Objetos que Crean Objetos ........................................................................................................ 4 3.10 Constructores Múltiples ............................................................................................................. 4 3.11 Llamadas a Métodos .................................................................................................................. 4 3.11.1 Llamada a métodos internos ............................................................................................... 4 3.11.2 Llamada a métodos externos .............................................................................................. 4 3.12 Otro ejemplo de interacción de objetos ..................................................................................... 5 3.12.2 La Palabra Clave this .......................................................................................................... 5 3.13 Uso del depurador, Debugger .................................................................................................... 6 7 Capítulo 4 Ag rupar Objetos Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 4 AgruparObjetos 4.1 La colección como abstracción Una colección es una abstracción y es el concepto de agrupar cosas para poder referirnos a ellas y manejarlas de manera conjunta. Una colección puede ser grande, pequeña o vacía. Operaciones típicas son: Añadir. Eliminar. Ordenar. La abstracción colección se convierte en una clase de algún tipo. Las operaciones serían los métodos de esa clase. Una colección de música Rock sería una instancia de la clase. Los elementos almacenados en una instancia de colección serían, ellos mismos, objetos. Necesitaremos una solución genérica para agrupar los objetos en colecciones. En ocasiones el número de elementos almacenados en la colección varía a lo largo del tiempo. Una solución adecuada sería aquella que no requiera que conozcamos anticipadamente la cantidad de elementos que queremos agrupar o bien, establecer un límite mayor que dicho número. 4.4 Utilización de una clase de librería Tenemos en cuenta que las clases de librería no aparecen en el diagrama de clases de BlueJ Los lenguajes orientados a objetos suelen estar acompañados de librerías de clases. Estas bibliotecas contienen varios cientos o miles de clases diferentes que han demostrado ser de gran ayuda para los desarrolladores en un amplio rango de proyectos diferentes. Java cuenta con varias de estas bibliotecas y las denomina paquetes, packages. La clase ArrayList, que es un ejemplo de una clase estándar de librería está definida en el paquete java.util. ArrayList es una clase de colección de propósito general, no está restringida en lo que respecta a los tipos de objeto que puede almacenar Las colecciones de objetos son objetos que pueden almacenar un número arbitrario de otros objetos. 4.4.1 Importación de una clase de librería El acceso a una clase estándar de librería de Java se obtiene mediante la sentencia import, para ArrayList sería: import java.util.ArrayList; Esta sentencia hace que la clase ArrayList del paquete java.util esté disponible para nuestra clase. Las sentencias import deben ubicarse en el texto de la clase, siempre antes del comienzo de la declaración de la clase. 2 Una vez que el nombre de una clase ha sido importado desde un paquete de esta manera, podemos usar dicha clase tal como si fuera una de nuestras propias clases. Por ejemplo como en la siguiente declaración: private ArrayList<String> files; Se lee: "una colección ArrayList de objetos tipo String" Aquí se observa una nueva estructura sintáctica: la mención de String entre símbolos de menor “<” y de mayor “>”: <String>. Cuando usamos colecciones, debemos especificar dos tipos: El tipo propio de la colección, en este caso ArrayList. El tipo de los elementos que planeamos almacenar en la colección, en este caso String. 4.4.2 Notación diamante Para instanciar un nuevo objeto se necesita especificar de nuevo el tipo completo con el tipo de elemento entre los símbolos de menor y de mayor, seguido de los paréntesis para la lista de parámetros: files = new ArrayList<String>(); Desde la versión 7 de Java el compilador puede inferir el tipo parametrizado del objeto que se está creando a partir del tipo de la variable a la que se está realizando la asignación. Se conoce como notación diamante: files = new ArrayList<>(); 4.4.3 Principales métodos de ArrayList La clase ArrayList declara muchos métodos entre ellos: add, size, get y remove. add almacena un objeto en la lista. size devuelve el número total de elementos almacenados en la lista. get devuelve un elemento, sin eliminarlo de la colección. remove elimina un objeto en la lista. 3 4.5 Estructuras de objetos con colecciones Características importantes de la clase ArrayList son: Es capaz de aumentar su capacidad interna tanto como se requiera: cuando se agregan más elementos, simplemente crea el espacio necesario para ellos. Mantiene su propia cuenta privada de la cantidad de elementos almacenados, que se obtiene mediante size. Mantiene el orden de los elementos que se agregan, el método add almacena cada nuevo elemento al final de la lista, posteriormente se pueden recuperar en el mismo orden. 4.6 Clases genéricas La nueva notación, ArrayList<String>, es una clase pero requiere que se especifique un segundo tipo como parámetro cuando se usa para declarar campos u otras variables. Las clases que requieren este tipo de parámetro se denominan clases genéricas. Las clases genéricas no definen un tipo único en Java sino potencialmente muchos tipos. Por ejemplo, la clase ArrayList puede usarse para especificar un ArrayList de Strings, un ArrayList de Personas, un ArrayList de Rectángulos, o un ArrayList de cualquier otra clase. Cada ArrayList en particular es un tipo distinto que puede usarse en declaraciones de campos, parámetros y tipos de retorno. private ArrayList<Persona> miembros; private ArrayList<MaquinaDeBoletos> misMaquinas; Estas declaraciones establecen que: miembros contiene un ArrayList que puede almacenar objetos Persona. misMaquinas contiene un ArrayList que almacena objetos MaquinaDeBoletos. Hay que tener en cuenta que ArrayList<Persona> y ArrayList<MaquinaDeBoletos> son tipos diferentes. Los campos no pueden ser asignados uno a otro, aun cuando sus tipos deriven de la misma clase. 4.7 Numeración dentro de las colecciones Los elementos almacenados en las colecciones tienen una numeración implícita o posicionamiento que comienza a partir de cero. La posición que ocupa un objeto en una colección se conoce como su índice. El primer elemento que se agrega a una colección tiene por índice al número 0, el segundo tiene al número 1, y así sucesivamente hasta size − 1. Los métodos que se utilicen deben asegurar que el valor de su parámetro se encuentre dentro del rango de valores de índice permitidos [0...size()−1]. Si se intenta acceder a un elemento de una colección que está fuera de los índices válidos del ArrayList se obtendrá un mensaje del error denominado desbordamiento, que en Java será IndexOutBoundsException. 4 4.7.1 El efecto de las eliminaciones sobre la numeración La clase ArrayList tiene un método remove que toma como parámetro el índice del elemento o el propio elemento a eliminar. Una complicación del proceso de eliminación es que se modifican los valores de los índices de los restantes objetos que están almacenados en la colección. También es posible insertar elementos en un ArrayList en otros lugares distintos que el final de la colección. Esto significa que los elementos que ya están en la lista deben incrementar sus índices cuando se agrega un nuevo elemento. Los usuarios deben ser conscientes de estos cambios en los índices cuando agregan o eliminan notas. 4.9 Procesamiento de una colección completa A la hora de procesar una colección y realizar una determinada acción varias veces utilizaremos instrucciones de bucle o estructuras iterativas de control. Un ciclo o bucle puede usarse para ejecutar repetidamente un bloque de sentencias sin tener que escribirlas varias veces. 4.9.1 El ciclo for-each for-each es un tipo de bucle. Las acciones de un ciclo for-each se pueden resumir en el siguiente pseudocódigo: for(TipoDelElemento elemento: colección){ cuerpo del ciclo } Consta de dos partes: Una cabecera del bucle (la primera línea del ciclo). Un cuerpo a continuación del encabezado. El cuerpo contiene aquellas instrucciones que queremos ejecutar una y otra vez. Una forma de entender este bucle sería: para cada elemento en la colección hacer: { cuerpo del bucle } En cada vuelta, antes de que la sentencia se ejecute, la variable elemento se configura para contener uno de los elementos de la lista: primero el del índice 0, luego el del índice 1, y así sucesivamente. La palabra clave for inicia el bucle, seguida por un par de paréntesis en los que se definen los detalles del bucle, lo primero vemos TipoDelElemento elemento, que declara una variable local elemento que se usará para almacenar los distintos elementos de la lista. Llamaremos variable de bucle a la variable que se usará para almacenar los elementos de la lista. El tipo de la variable de bucle debe ser el mismo que el tipo del elemento declarado para la colección que estamos usando, a continuación aparecen dos puntos y la variable que contiene la colección que deseamos procesar. Cada elemento de esta colección será asignado en su turno a la variable de bucle, y para cada una de estas asignaciones el cuerpo del bucle se ejecutará una sola vez. 5 4.10 Iteración Indefinida Una acción se repetirá un número de veces no predecible hasta que se complete la tarea. 4.10.1 El bucle while Un bucle while consta de una cabecera y de un cuerpo, el cuerpo puede ejecutarse repetidamente. La estructura de un bucle while sería: while (condición booleana){ cuerpo del ciclo } El ciclo while comienza con la palabra clave while, seguida de una condición booleana. Este ciclo es más flexible que el ciclo for-each. Puede recorrer un número variable de elementos de la colección, dependiendo de la condición del bucle. La condición booleana es una expresión lógica que se usa para determinar si el cuerpo debe ejecutarse al menos una vez. Si la condición se evalúa verdadera, se ejecuta el cuerpo del ciclo. Cada vez que se ejecuta el cuerpo del ciclo, la condición se vuelve a controlar nuevamente. Este proceso continúa repetidamente hasta que la condición resulta falsa, que es el punto en el que se salta del cuerpo del ciclo y la ejecución continúa con la sentencia que esté ubicada inmediatamente después del cuerpo. 4.10.2 Iteración mediante una variable índice Utilizar un bucle while requiere más esfuerzo de programación: Hay que declarar fuera del bucle una variable para el índice e iniciarlo por nuestros propios medios a 0 para acceder al primer elemento de la lista. La condición tiene que estar bien definida, sino el bucle será infinito. También tenemos que llevar nuestra propia cuenta del índice para recordar la posición en que estábamos. Existe un operador especial para incrementar una variable numérica en 1: variable++; Que es equivalente a: variable = variable + 1; Hay dos puntos más a destacar sobre el bucle while: No necesita estar relacionado con una colección. No necesitamos procesar cada uno de sus elementos de la colección. Una ventaja de tener una variable de índice explícita es que podemos utilizar su valor tanto dentro como fuera del bucle. Una variable de índice local nos resultará útil a la hora de realizar búsquedas en una lista: Nos proporciona información dónde está ubicado el elemento Podemos hacer que esta información siga estando disponible una vez que el bucle haya finalizado. 6 4.10.3 Búsquedas en una colección La característica clave de una búsqueda infinita es que implica una iteración indefinida. En situaciones reales de búsqueda consideramos dos posibilidades: La búsqueda tiene éxito después de un número indefinido de iteraciones. La búsqueda falla después de agotar todas las posibilidades. Uno de estos dos criterios debe evaluar la condición como false para detener el bucle. 4.10.4 Algunos ejemplos no relacionados con colecciones Los bucles podemos utilizarlos en situaciones distintas a la iteración de una colección. Los bucles each-for no son válidos para este propósito. Podemos usar el bucle while o el bucle for. 4.12 El tipo iterador Existe una tercera variante para recorrer una colección, que está entre medio de los ciclos while y for-each. Usa un ciclo while para llevar a cabo el recorrido y un objeto iterador en lugar de una variable entera como índice para controlar la posición dentro de la lista. Iterator, con la I mayúscula es un tipo de Java, también existe el método iterator. Un iterador es un objeto que proporciona funcionalidad para recorrer todos los elementos de una colección. El método iterator de ArrayList devuelve un objeto Iterator. La clase Iterator también está definida en el paquete java.util. Para poder utilizarlo: Import java.util.ArrayList; Import java.util.Iterator; Un Iterator provee dos métodos para iterar una colección: hasNext y next. La manera de uso en pseudocódigo sería: Iterator<TipoDelElemento> it = miColeccion.iterator (); while (it.hasNext ( )) { Invocar it. next () para obtener el siguiente elemento Hacer algo con dicho elemento } Iterator también es de tipo genérico por lo que hay que parametrizarlo con el tipo de los elementos de la colección. Luego usamos dicho iterador para controlar repetidamente si hay más elementos it.hastNext() y para obtener el siguiente elemento it.next(). Un punto a destacar es que le pedimos al iterador que devuelva el siguiente elemento y no a la colección. La llamada a next hace que el objeto Iterator devuelva el siguiente elemento de la colección y luego avance más allá de ese elemento. 7 4.12.1 Comparación entre los iteradores y el acceso mediante índices El bucle for-each, es la técnica estándar que se usa si deben procesarse todos los elementos de una colección porque es el más breve para este caso, pero la menos flexible. El bucle while (con un índice y el método get) y el iterador tienen la ventaja de que la iteración puede ser detenida más fácilmente en mitad del proceso, de modo que son mejores cuando se quiere procesar sólo una parte de una colección. Para algunas colecciones, es imposible o muy ineficiente acceder a elementos individuales mediante un índice y se accede mediante for-each o iterador. El Iterator está disponible para todas las colecciones de las clases de las bibliotecas de Java y es un patrón importante que se usará a menudo. 4.12.2 Eliminación de elementos Puede darse el caso que necesitemos eliminar elementos de la colección mientras estamos iterando. La solución apropiada es utilizar un Iterator. Tiene un tercer método, remove. No admite ningún parámetro y tiene un tipo de retorno void. Invocar a remove hará que sea eliminado el elemento devuelto por la llamada más reciente a next. Este tipo de eliminación no es posible con el bucle for-each ya que no dispone de un Iterator con el que trabajar. Podemos utilizar el bucle while con un Iterator. 4.14 Otro ejemplo: un sistema de subastas 4.14.1 La palabra clave null. La palabra clave null se usa para significar que «no hay objeto», es decir cuando una variable objeto no está haciendo referencia realmente ningún objeto. Un campo que no haya sido inicializado explícitamente contendrá el valor por defecto null. 4.14.5 Objetos anónimos La siguiente sentencia ilustra el uso de objetos anónimos: miColeccion.add(new Elemento()); Aquí estamos haciendo dos cosas: Crear un nuevo objeto Elemento Pasar este nuevo objeto al método add de miColeccion. Podríamos haber escrito lo mismo en dos líneas de código, una para declarar el nuevo elemento y otra para pasárselo a la colección. Ambas versiones son equivalentes, pero la primera versión evita declarar una variable que puede tener un uso muy limitado. Se crea un objeto anónimo, un objeto sin nombre, pasándoselo directamente al método que lo utiliza. Dos objetos String s1 y s2 pueden compararse para ver si son iguales mediante la expresión lógica: s1.equals(s2). 8 4.16 Colecciones de tamaño fijo A veces conocemos anticipadamente cuántos elementos deseamos almacenar en la colección y este número permanece invariable durante la vida de la colección. En estas circunstancias, tenemos la opción de utilizar una colección de objetos de tamaño fijo. Una colección de tamaño fijo se denomina array o vector. Un vector es un tipo especial de colección que puede almacenar un número fijo de elementos. Se obtienen ventajas con respecto a las clases de colecciones de tamaño flexible: El acceso a los elementos de un vector es generalmente más eficiente que el acceso a los elementos de una colección de tamaño flexible. Los vectores son capaces de almacenar objetos o valores de tipos primitivos. Las colecciones de tamaño flexible sólo pueden almacenar objetos. 4.16.2 Declaración de variables vectores La característica distintiva de la declaración de una variable de tipo vector es un par de corchetes que forman parte del nombre del tipo: int[]. Este detalle indica que la variable declarada es de tipo vector de enteros. Decimos que int es el tipo base de este vector en particular. La declaración de una variable vector no crea en sí misma un objeto vector, sólo reserva un espacio de memoria para que en un próximo paso, usando el operador new, se cree el vector tal como con los otros objetos. 4.16.3 Creación de objetos vector La siguiente sentencia muestra como se asocia una variable vector con un objeto vector: obejetoVector = new int[tamaño]; La forma general de la construcción de un objeto vector es: new tipo[expresión-entera] La elección del tipo especifica de qué tipo serán todos los elementos que se almacenarán en el vector. La expresión-entera especifica el tamaño del vector. Cuando se asigna un objeto vector a una variable vector, el tipo del objeto vector debe coincidir con la declaración del tipo de la variable. Cuando se crea un vector no crea tantos objetos como tiene capacidad para almacenar, solo crea una colección de tamaño fijo que es capaz de almacenar dichos objetos. 4.16.4 Utilizar objetos de vector Se accede a los elementos individuales de un objeto vector mediante un índice. Un índice es una expresión entera escrita entre un par de corchetes a continuación del nombre de una variable vector. Los valores válidos para una expresión que funciona como índice dependen de la longitud del vector en el que se usarán. Los índices de los vectores siempre comienzan por cero y van hasta el valor del tamaño del vector menos uno. 9 Las expresiones que seleccionan un elemento de un vector se pueden usar en cualquier lugar que requiera un valor del tipo base del vector. Esto quiere decir que podemos usarlas, por ejemplo, en ambos lados de una asignación. El uso de un índice de un vector en el lado izquierdo de una asignación es equivalente a un método mutador (o método set) del vector porque cambiará el contenido del mismo. Los restantes usos del índice son equivalentes a los métodos selectores (o métodos get). 4.16.5 El bucle for Java define dos variantes para el ciclo for, el bucle for-each, y el bucle for, que es una estructura de control repetitiva alternativa que resulta particularmente adecuada cuando: Queremos ejecutar un conjunto de sentencias un número exacto de veces. Necesitamos una variable dentro del ciclo cuyo valor cambie en una cantidad fija, generalmente en 1, en cada iteración. Es común el uso del ciclo for cuando queremos hacer algo con cada elemento de un vector tal como imprimir el contenido de cada elemento. Un ciclo for tiene la siguiente forma general: for(inicialización;condición;incremento){ instrucciones a repetir } En este ciclo for, los paréntesis contienen tres secciones distintas separadas por símbolos de punto y coma (;). Su equivalente while: Inicialización; while (condición){ Instrucciones a repetir incremento } Todos los vectores contienen un campo length que contiene el valor del tamaño del vector. El valor de este campo coincide siempre con el valor entero usado para crear el objeto vector. Por eso la condición del bucle suele usar el operador menor que “<” para controlar el valor del índice respecto de la longitud del vector. Por lo general, cuando deseamos acceder a cada elemento de un vector, el encabezado del bucle for tendrá la siguiente forma: for (int indice = 0; indice < vector.length; indice ++) 10 4.17 ¿Qué ciclo debo usar? Si se necesita recorrer todos los elementos de una colección, el ciclo for-each es, casi siempre, el ciclo más elegante para usar, aunque no provee una variable contadora de ciclo. Si se tiene un bucle que no está relacionado con una colección (pero lleva a cabo un conjunto de acciones repetidamente), el ciclo for-each no resulta útil. En este caso, se puede elegir entre el ciclo for y el ciclo while. El ciclo for-each es sólo para colecciones. El ciclo for es bueno si conoce anticipadamente la cantidad de repeticiones necesarias. Esta información puede estar dada por una variable, pero no puede modificarse durante la ejecución del ciclo. Este ciclo también resulta muy bueno cuando necesita usar explícitamente una variable contadora. El ciclo while será el adecuado si, al comienzo del ciclo, no se conoce la cantidad de iteraciones que se deben realizar. El fin del ciclo puede determinarse previamente mediante alguna condición. Si tenemos que eliminar elementos de la colección mientras la recorremos en bucle, conviene utilizar un bucle for con un Iterator si se quiere examinar la colección completa, o un bucle while cuando queramos terminar antes de alcanzar el final de la colección. 11 4 Agrupar Objetos ............................................................................................................................... 2 4.1 La colección como abstracción .................................................................................................. 2 4.4 Utilización de una clase de librería ............................................................................................ 2 4.4.1 Importación de una clase de librería................................................................................... 2 4.4.2 Notación diamante .............................................................................................................. 3 4.4.3 Principales métodos de ArrayList ...................................................................................... 3 4.5 Estructuras de objetos con colecciones...................................................................................... 4 4.6 Clases genéricas ......................................................................................................................... 4 4.7 Numeración dentro de las colecciones ...................................................................................... 4 4.7.1 4.9 El efecto de las eliminaciones sobre la numeración ........................................................... 5 Procesamiento de una colección completa ................................................................................ 5 4.9.1 El ciclo for-each ................................................................................................................. 5 4.10 Iteración Indefinida .................................................................................................................... 6 4.10.1 El bucle while ..................................................................................................................... 6 4.10.2 Iteración mediante una variable índice ............................................................................... 6 4.10.3 Búsquedas en una colección ............................................................................................... 7 4.10.4 Algunos ejemplos no relacionados con colecciones .......................................................... 7 4.12 El tipo iterador ........................................................................................................................... 7 4.12.1 Comparación entre los iteradores y el acceso mediante índices ........................................ 8 4.12.2 Eliminación de elementos .................................................................................................. 8 4.14 Otro ejemplo: un sistema de subastas ........................................................................................ 8 4.14.1 La palabra clave null. ...................................................................................................... 8 4.14.5 Objetos anónimos ............................................................................................................... 8 4.16 Colecciones de tamaño fijo ........................................................................................................ 9 4.16.2 Declaración de variables vectores ...................................................................................... 9 4.16.3 Creación de objetos vector ................................................................................................. 9 4.16.4 Utilizar objetos de vector ................................................................................................... 9 4.16.5 El bucle for ....................................................................................................................... 10 4.17 ¿Qué ciclo debo usar? .............................................................................................................. 11 12 Capítulo 5 Comportamientos mássofisticados Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 5 Comportamientosmássofisticados 5.1 Documentación de las clases de librería La librería de Java contiene muchas clases que son muy útiles. Es importante: Conocer algunas de las clases más importantes de la biblioteca por su nombre. La forma de encontrar otras clases y buscar sus detalles. Lo más importante es que tenemos que ser capaces de explorar y comprender la biblioteca por nuestros propios medios. Es importante poder leer y comprender la documentación Java. Las clases las utilizamos sin mirar su código fuente; no es necesario para usar su funcionalidad. Todo lo que necesitamos saber es el nombre de la clase, los nombres de los métodos, los parámetros y los tipos de retorno de los métodos y saber exactamente qué hacen estos métodos. La misma cuestión es cierta para otras clases no estándar en proyectos de software grandes. Generalmente, algunas personas trabajan juntas en un proyecto pero trabajando sobre partes diferentes. Cada miembro del equipo debe escribir la documentación de la clase en forma similar a la documentación de la biblioteca estándar de Java de modo que permita a otras personas usar la clase sin necesidad de leer el código. 5.3 Lectura de la documentación de las clases 5.3.1 Interfaces e implementación La documentación incluye diferentes elementos de información: El nombre de la clase. Una descripción general del propósito de la clase. Una lista de los constructores y los métodos de la clase. Los parámetros y los tipos de retorno de cada constructor y de cada método. Una descripción del propósito de cada constructor y cada método. Toda esta información conjunta recibe el nombre de interfaz de la clase. La interfaz de una clase describe lo que es capaz de hacer dicha clase y la manera en que se puede usar sin mostrar su implementación. La interfaz no muestra el código con que está implementada la clase. Estamos nuevamente frente a la abstracción en acción. El código completo que define una clase se denomina la implementación de dicha clase. También se utiliza la terminología interfaz referida a métodos individuales. La interfaz de un método consta de la signatura y un comentario. 2 La signatura de un método incluye, en este orden: Un modificador de acceso, (public, private,…). El tipo de retorno del método. El nombre del método. Una lista de parámetros. La interfaz de un método proporciona todos los elementos necesarios para saber cómo usarlo. 5.3.2 Utilización de métodos de clases de librería Se dice que un objeto es inmutable si su contenido o su estado no puede ser cambiado una vez que se ha creado. Los objetos String, cadenas de caracteres, son un ejemplo de objetos inmutables. entrada = entrada.trim(); Este código le solicita al objeto almacenado en la variable entrada crear una nueva cadena similar a la dada, pero eliminados los espacios en blanco antes y después de la palabra. Ahora podemos insertar esta línea en nuestro código de modo que quede así: String entrada = lector.getEntrada(); entrada = entrada.trim(); Las primeras dos líneas podrían unirse para formar una sola línea: String entrada = lector.getEntrada().trim(); 5.3.3 Comprobación de la igualdad entre cadenas Con variables de objeto, el operador (==) evalúa si ambos operandos hacen referencia al mismo objeto, no si sus valores son iguales. Esta es una diferencia importante. La solución para este problema es usar el método equals, que deben tener todas las clases, ya que al menos lo heredan de la Clase Object que es superclase de todas las demás. 5.4 Adición de comportamiento aleatorio Aleatorio y pseudo-aleatorio: Las computadoras operan de una manera bien definida y determinística que se apoya en el hecho de que todo cálculo es predecible y repetible, en consecuencia existe poco espacio para un comportamiento realmente aleatorio. Se han propuesto muchos algoritmos para producir secuencias semejantes a los números aleatorios. Estos números no son típicamente números aleatorios verdaderos, aunque siguen reglas muy complicadas. Estos números se conocen como números pseudo-aleatorios. En Java la generación de números pseudoaleatorios ha sido implementada en una clase de la biblioteca. 3 5.4.1 La clase Random La biblioteca de clases de Java contiene una clase de nombre Random capaz de generar datos pseudoaleatorios. Para generar un número aleatorio tenemos que: Crear una instancia de la clase Random. Hacer una llamada a un método de esa instancia para obtener un número. Ejemplo de uso: Random randomGenerator; randomGenerator = new Random(); int indice = randomGenerator.nextInt () ; System.out.println(indice); Este fragmento de código crea una nueva instancia de la clase Random y la almacena en la variable randomGenerator. Luego, invoca al método nextInt de esta variable para obtener un número por azar, almacena el número generado en la variable indice y eventualmente lo imprime en pantalla. 5.4.2 Números aleatorios con rango limitado Lo más frecuente es que necesitemos números aleatorios dentro de un rango limitado específico. La clase Random posee el método nextInt, pero con un parámetro para especificar el rango de números que queremos usar. nextInt(int n) El método nextInt(int n) de la clase Random de la biblioteca de Java especifica que genera números desde 0 (inclusive) hasta n (exclusive). Esto quiere decir que el valor 0 está incluido entre los posibles valores de los resultados, mientras que el valor especificado por n no está incluido. El máximo número posible que devuelve es n − 1. 5.4.3 Lectura de la documentación de las clases parametrizadas En la ayuda, algunos nombres de las clases que aparecen en la lista de la documentación tienen un formato ligeramente diferente, tal es el caso de ArrayList<E>. Las clases similares a éstas se denominan clases parametrizadas o clases genéricas. La información contenida entre los símbolos de menor y de mayor nos dice que, cuando usemos estas clases deberemos suministrar uno o más nombres de tipos entre dichos símbolos, para completar la definición. Por lo tanto, si busca en la lista de métodos de ArrayList<E> verá métodos tales como: boolean add(E o) E get(int index) Estas signaturas nos indican que el tipo de objetos que podemos agregar u obtener de un ArrayList depende del tipo usado para parametrizarlo. 4 5.5 Paquetes e importación Las clases de Java almacenadas en la librería de clases no están disponibles automáticamente para su uso, tal como las otras clases del proyecto actual. Para poder disponer de ellas, debemos explicitar en nuestro código que queremos usar una clase de la librería. Esta acción se denomina importación de la clase y se implementa mediante la sentencia import. La sentencia import tiene la forma general: import nombre-de-clase-calificado; Java utiliza paquetes (packages) para agrupar las clases de la librería en grupos de clase relacionadas. Los paquetes pueden estar anidados, es decir, los paquetes pueden contener otros paquetes. El nombre completo o nombre cualificado de una clase es el nombre de su paquete, seguido por un punto y por el nombre de la clase, por ejemplo: java.util.ArrayList java.util.Random Java también nos permite importar paquetes completos con sentencias de la forma: import nombre-del-paquete.*; por lo que la siguiente sentencia importaría todas las clases del paquete java.util: import java.util.*; La enumeración de todas las clases utilizadas separadamente da un poco más de trabajo en términos de escritura pero funciona bien como parte de la documentación. Existe una excepción a esta regla: algunas clases se usan tan frecuentemente que casi todas las clases deberían importarlas. Estas clases se han ubicado en el paquete java.lang y este paquete se importa automáticamente dentro de cada clase. La clase String es un ejemplo de una clase ubicada en java.lang. 5.6 Utilización de mapas para asociaciones 5.6.1 Concepto de mapa Un mapa es una colección que almacena parejas clave/valor de objetos. Los valores se pueden buscar proporcionando la clave. Un mapa puede almacenar un número flexible de entradas. En un Map cada entrada no es un único objeto sino una pareja de objetos. Esta pareja está compuesto por un objeto clave y un objeto valor. En lugar de buscar las entradas en esta colección mediante un índice entero usamos el objeto clave para buscar el objeto valor. Un mapa puede organizarse de manera tal que resulte fácil buscar en él un valor para una clave. Los mapas son ideales para una única forma de búsqueda, en la que conocemos la clave a buscar y necesitamos conocer solamente el valor asociado a esta clave. 5 5.6.2 Usar un HashMap Un HashMap es una implementación específica de un Map. Los métodos más importantes de la clase HashMap son put y get. El método put inserta una entrada en el mapa y el método get recupera el valor correspondiente a una clave determinada. HashMap<String, String> phoneBook = new HashMap<String, String>(); phoneBook.put ("Charles James",(531) 2356 8547"); phoneBook.put ("Lisa Jones",(402) 4536 4674"); phoneBook.put ("William Smith",(998) 4555 5219"); la especificación de tipo genérico del lado derecho de la asignación puede omitirse, como en la siguiente instrucción: HashMap<String, String> phoneBook = new HashMap<>(); Esto se conoce con el nombre de operador diamante. Si a un HashMap se le proporciona un par clave, valor, con una clave ya existente se reemplaza el valor anterior de esa clave. Si se intenta acceder al valor correspondiente a una clave inexistente, devuelve null; también se pueden almacenar valores nulos, o incluso guardar una clave null, por lo que existe el método boolean containsKey(E clave), que nos informa si un mapa contiene una determinada clave. El método int size() devuelve el número de pares clave, valor almacenados. 5.7 Utilización de conjuntos La librería estándar de Java incluye diferentes variantes de conjuntos, implementados en clases diferentes. Una de ellas es HashSet. Un conjunto es una colección que almacena cada elemento individual una sola vez como máximo. No mantiene un orden específico. Los dos tipos de funcionalidad que necesitamos de un conjunto son: Ingresar elementos en él y más tarde, Recuperar estos elementos. import java.util.HashSet; HashSet<String> miConjunto = new HashSet<String>(); miConjunto.add("uno"); miConjunto.add("dos"); Comparamos este código con las sentencias que necesitamos para entrar elementos en un ArrayList. No hay prácticamente ninguna diferencia, excepto que esta vez creamos un HashSet en lugar de un ArrayList. for(String miConjunto) { Hacer algo con cada elemento } 6 Las diferencias reales residen en el comportamiento de cada colección. Por ejemplo, una lista contiene todos los elementos ingresados en el orden deseado, provee acceso a sus elementos a través de un índice y puede contener el mismo elemento varias veces. Por otro lado, un conjunto no mantiene un orden específico (el iterador puede devolver los elementos en diferente orden del que fueron ingresados) y asegura que cada elemento en el conjunto está una única vez. En un conjunto, el ingresar un elemento por segunda vez simplemente no tiene ningún efecto. List, Map y Set. Cuando tratamos de comprender la forma en que se usan las diferentes clases de colecciones, la segunda parte del nombre es la mejor indicación de los datos que almacenan, y la primera palabra describe la forma en que se almacenan. Generalmente estamos más interesados en el “qué” (la segunda parte) antes que en el “cómo”. De modo que un TreeSet debiera usarse de manera similar a un HashSet, mientras que un TreeMap debiera usarse de manera similar a un HashMap. 5.8 División de cadenas de caracteres El método split de la clase String puede dividir una cadena en distintas subcadenas y las devuelve en un array de cadenas. El parámetro del método split establece la clase de caracteres de la cadena original que producirá la división en palabras. 5.10 Escritura de la documentación de las clases La documentación de una clase debiera ser suficientemente detallada como para que otros programadores puedan usarla sin tener que leer su implementación. El sistema Java incluye una herramienta denominada javadoc que se puede utilizar para generar la interfaz que describa nuestros archivos fuente. 5.10.1 Utilización de javadoc en BlueJ El entorno BlueJ utiliza javadoc para posibilitar la creación de la documentación de las clases de dos formas: Podemos ver la documentación para una única clase pasando el selector emergente situado en la parte superior derecha de la ventana del editor de Source Code a Documentation o seleccionando Toggle Documentatiton View. Podemos usar la función Project Documentation disponible en el menú Tools de la ventana principal para generar la documentación correspondiente a todas las clases del proyecto. 5.10.2 Elementos de la documentación de una clase La documentación de una clase debe incluir como mínimo: El nombre de la clase. Un comentario que describa el propósito general y las características de la clase. Un número de versión. El nombre del autor (o autores). La documentación de cada constructor y de cada método. 7 La documentación de cada constructor y de cada método debe incluir: El nombre del método. El tipo de retorno. Los nombres y tipos de los parámetros. Una descripción del propósito y de la función del método. Una descripción de cada parámetro. Una descripción del valor que devuelve. Además, cada proyecto debe tener un comentario general, frecuentemente guardado en un archivo de nombre “Leeme” o “ReadMe”. En Java, los comentarios de estilo javadoc se escriben con un símbolo especial de al comienzo: /** Este es un comentario javadoc */ El símbolo de inicio de un comentario debe tener dos asteriscos para que javadoc lo reconozca. Este tipo de comentario, ubicado inmediatamente antes de la declaración de clase es interpretado como un comentario de clase. Si el comentario está ubicado directamente arriba de la signatura de un método, es considerado como un comentario de método. En Java y mediante javadoc, se dispone de varios símbolos especiales para dar formato a la documentación. Estos símbolos comienzan con el símbolo @ e incluyen entre otros: @version @autor @param @return 5.11 públic y private Los modificadores de acceso son las palabras clave como public o private que aparecen al comienzo de las declaraciones de campos y de las signaturas de los métodos. Los modificadores de acceso definen la visibilidad de un campo, de un constructor o de un método. Los elementos públicos son accesibles dentro de la misma clase o fuera de ella. Los elementos privados son accesibles solamente dentro de la misma clase. Los campos, los constructores y los métodos pueden ser públicos o privados; a la mayoría de los campos suelen ser privados y la mayoría de los constructores y de los métodos suelen ser públicos. La interfaz de una clase es el conjunto de detalles que necesita ver otro programador que utilice dicha clase. La interfaz puede verse como la parte pública de una clase. Su propósito es definir lo que hace la clase. La implementación es la sección de una clase que define precisamente cómo funciona la clase. También nos referimos a la implementación como la parte privada de una clase. El usuario de una clase no necesita conocer su implementación. En realidad, existen buenas razones para evitar que un usuario conozca la implementación o por lo menos, que use ese conocimiento. Este principio se denomina ocultamiento de la información. 8 La palabra clave public declara que un elemento de una clase, un campo o un método, forma parte de la interfaz, es decir, es visible públicamente. La palabra clave private declara que un elemento es parte de la implementación, es decir, permanece oculto para los accesos externos. 5.11.1 Ocultamiento de la información El ocultamiento de la información es un principio que establece que los detalles internos de implementación de una clase deben permanecer ocultos para las otras clases. Garantiza una mejor modularización de la aplicación. En muchos lenguajes de programación orientados a objetos, el interior de una clase, su implementación, permanece oculta para las otras clases. Hay dos aspectos en este punto: Primero, un programador que hace uso de una clase no necesita conocer su interior; Segundo, a un usuario no se le permite conocer los detalles internos. El primer principio, necesidad de conocer, tiene que ver con la abstracción y la modularización. Si necesitáramos conocer todos los detalles internos de todas las clases que queremos usar, no terminaríamos nunca de implementar sistemas grandes. El segundo principio, que no se permite conocer, es diferente. También tiene que ver con la modularización pero en un contexto diferente. El lenguaje de programación no permite el acceso a una sección privada de una clase mediante sentencias en otra clase. Esto asegura que una clase no dependa de cómo está implementada exactamente otra clase. Este punto es muy importante para el trabajo de mantenimiento. Una tarea muy común de mantenimiento de un programa es la modificación o extensión de la implementación de una clase para mejorarlo o para solucionar defectos. Idealmente, las modificaciones en la implementación de una clase no debieran generar la necesidad de cambiar también las otras clases. Esta característica se conoce como acoplamiento. Si se cambia una parte de un programa no debiera ser necesario hacer cambios en otras partes del programa, cuestión que se conoce como fuerte y débil acoplamiento. El acoplamiento débil es bueno porque hace que el trabajo de mantenimiento del programador sea mucho más fácil, en lugar de comprender y modificar muchas clases, deberá comprender y modificar sólo una clase. Para ser más precisos, la regla de que a un usuario «no se le debe permitir conocer el interior de una clase» no se refiere al programador de otras clases sino a la clase en sí misma. Generalmente, no es un problema el hecho de que un programador conozca los detalles de implementación, pero una clase no debiera “conocer”, ser dependiente de, los detalles internos de otras clases. El programador de ambas clases podría ser hasta la misma persona pero las clases aún tendrían que permanecer débilmente acopladas. La palabra clave private provoca el ocultamiento de la información al impedir el acceso a esta parte de la clase desde otras clases. Esto asegura el acoplamiento débil y hace que la aplicación resulte más modular y más fácil de mantener. 9 5.11.2 Métodos privados y campos públicos En general los métodos de las clases son públicos, aunque no siempre es así. A veces existen métodos de apoyo dentro de una clase que son usados por otros métodos de dicha clase; normalmente esas subtareas no tienen la finalidad de ser invocadas directamente desde el exterior de la clase pero se las ubica como métodos separados con la intencionalidad de lograr que la implementación de una clase sea más fácil de leer. En este caso, tales métodos deben ser privados. Otra buena razón para tener un método privado es cuando una tarea necesita ser usada (como una subtarea) en varios métodos de una clase. En lugar de escribir el código varias veces, podemos escribirlo una única vez en un solo método privado y luego invocar este método desde diferentes lugares de la clase. En Java, los campos también pueden ser declarados privados o públicos. La declaración de los campos como públicos rompe con el principio de ocultamiento de la información. Hace que una clase que depende de esa información sea vulnerable a operaciones incorrectas, si se modifica la implementación. Una razón más para mantener los campos como privados reside en que permiten que un objeto crezca manteniendo el control sobre su estado. Si el acceso a los campos privados se canaliza a través de métodos selectores y de mutadores, entonces un objeto tiene la habilidad de asegurar que el campo nunca se configura con un valor que resulte inconsistente con su estado. Este nivel de integridad no es posible si los campos son públicos. Abreviando, los campos debieran ser siempre privados. 5.13 Variables de clase y constantes 5.13.1 La palabra clave static La palabra clave static se usa en la sintaxis de Java para definir variables de clase. Las variables de clase son campos que se almacenan en la misma clase y no en un objeto. Este hecho produce diferencias fundamentales con respecto a las variables de instancia. public class MiClase { <acceso> static <tipo> <identificador>; … } Las clases pueden tener campos. Estos campos se conocen como variables de clase o variables estáticas. En todo momento, existe exactamente una copia de una variable de clase, independientemente del número de instancias que se hayan creado de dicha clase. El código de la clase puede acceder, leer y configurar, a esta clase de variable de la misma forma en que accede a las variables de instancia. Se puede acceder a la variable de clase desde cualquiera de las instancias de la clase; como resultado, los objetos comparten esta variable. Las variables de clase se usan frecuentemente en los casos en que un valor debe ser siempre el mismo para todas las instancias de una clase. 10 En lugar de almacenarse una copia con el mismo valor en cada objeto, que sería un desperdicio de espacio y puede ser más difícil de coordinar, puede compartirse un único valor entre todas las instancias. Java también soporta métodos de clase, es decir métodos a los que se accede directamente desde la clase y que no necesitan una instancia para ser llamados. 5.13.2 Constantes Un uso frecuente de la palabra clave static se produce en la declaración de constantes. Las constantes son similares a las variables pero no pueden cambiar su valor durante la ejecución de una aplicación. En Java, las constantes se definen con la palabra clave final: private final int SIZE = 10; En esta instrucción definimos una constante de nombre SIZE con el valor 10. Observamos que las declaraciones de constantes son similares a las declaraciones de campos pero con dos diferencias: Deben incluir la palabra clave final antes del nombre del tipo. Deben ser inicializadas con un valor en el momento de su declaración (o en los constructores de la clase). Si se pretende que un valor no cambie nunca, es una buena idea declararlo como final. Esto garantiza que no pueda ser modificado posteriormente de manera accidental. Cualquier intento posterior de cambiar un campo constante dará por resultado un mensaje de error en tiempo de compilación. Por convención, las constantes se escriben frecuentemente con letras mayúsculas. En la práctica, es muy frecuente el caso en que las constantes se relacionen con todas las instancias de una clase. En esta situación declaramos constantes de clase. Las constantes de clase son campos de clase constantes. Se declaran usando una combinación de las palabras clave static y final. private static final int TOPE = 10; 11 5 Comportamientos más sofisticados .................................................................................................. 2 5.1 Documentación de las clases de librería .................................................................................... 2 5.3 Lectura de la documentación de las clases ................................................................................ 2 5.3.1 Interfaces e implementación ............................................................................................... 2 5.3.2 Utilización de métodos de clases de librería ...................................................................... 3 5.3.3 Comprobación de la igualdad entre cadenas ...................................................................... 3 5.4 Adición de comportamiento aleatorio ....................................................................................... 3 5.4.1 La clase Random ................................................................................................................ 4 5.4.2 Números aleatorios con rango limitado.............................................................................. 4 5.4.3 Lectura de la documentación de las clases parametrizadas ................................................ 4 5.5 Paquetes e importación .............................................................................................................. 5 5.6 Utilización de mapas para asociaciones .................................................................................... 5 5.6.1 Concepto de mapa .............................................................................................................. 5 5.6.2 Usar un HashMap ............................................................................................................... 6 5.7 Utilización de conjuntos ............................................................................................................ 6 5.8 División de cadenas de caracteres ............................................................................................. 7 5.10 Escritura de la documentación de las clases .............................................................................. 7 5.10.1 Utilización de javadoc en BlueJ ......................................................................................... 7 5.10.2 Elementos de la documentación de una clase .................................................................... 7 5.11 públic y private .......................................................................................................................... 8 5.11.1 Ocultamiento de la información ......................................................................................... 9 5.11.2 Métodos privados y campos públicos .............................................................................. 10 5.13 Variables de clase y constantes................................................................................................ 10 5.13.1 La palabra clave static ................................................................................................ 10 5.13.2 Constantes ........................................................................................................................ 11 12 Capítulo 6 Di seño deClases Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 6 DiseñodeClases 6.1 Introducción Es posible implementar una aplicación y lograr que realice su tarea mediante un diseño de clases mal diseñado. El problema surge, típicamente, cuando un programador de mantenimiento quiere hacer algunos cambios en una aplicación existente. Si por ejemplo, el programador intenta solucionar un fallo o quiere agregar una nueva funcionalidad a un programa, una tarea que debiera ser fácil y obvia con un buen diseño de clases, podría resultar muy difícil de manejar y consumir una gran cantidad de trabajo si las clases están mal diseñadas. En las aplicaciones grandes, este efecto ya ocurre durante la implementación original. Si la implementación comienza con una mala estructura, su finalización puede volverse muy compleja y puede que no se termine de completar el programa, o que contenga fallos o que su construcción tome más tiempo de lo necesario. El diseño de mala calidad no es, generalmente, consecuencia de tener un problema difícil para resolver. La mala calidad del diseño tiene más que ver con las decisiones que se toman cuando se resuelve un problema en especial. No podemos usar el argumento de que no había otra manera de resolver el problema como una excusa para un diseño de mala calidad. 6.3 Introducción al acoplamiento y a la cohesión Hay dos términos que son fundamentales cuando hablamos sobre la calidad de un diseño de clases: acoplamiento y cohesión. El término acoplamiento describe la interconexión de las clases. Nos esforzamos por lograr acoplamiento débil en un sistema, es decir, un sistema en el que cada clase sea altamente independiente y se comunique con otras clases mediante una interfaz compacta y bien definida. El grado de acoplamiento determina el grado de dificultad de realizar modificaciones en una aplicación. En una estructura de clases fuertemente acopladas, un cambio en una clase hace necesario también cambiar otras varias clases. Este hecho es el que tratamos de evitar porque el efecto de hacer un pequeño cambio puede rápidamente propagarse a la aplicación completa. Además, encontrar todos los lugares en que resulta necesario hacer los cambios y realmente llevar a cabo estos cambios puede ser dificultoso y consumir demasiado tiempo. Por otro lado, en un sistema débilmente acoplado, podemos con frecuencia modificar una clase sin tener que realizar cambios en ninguna otra y la aplicación continúa funcionando. El término cohesión describe cuánto se ajusta una unidad de código a una tarea lógica o a una entidad. En un sistema altamente cohesivo cada unidad de código (método, clase o módulo) es responsable de una tarea bien definida o de una entidad. Un diseño de clases de buena calidad exhibe un alto grado de cohesión. La cohesión es relevante para unidades formadas por una sola clase y para métodos individuales, así como para módulos o paquetes. Idealmente, una unidad de código debiera ser responsable de una tarea coherente, es decir, una tarea que pueda ser vista como una unidad lógica. Un método debiera implementar una operación lógica y una clase debiera representar un tipo de entidad. 2 La razón principal que subyace al principio de cohesión es la reutilización: si un método de una clase es responsable de una única cosa bien definida es más probable que pueda ser usado nuevamente en un contexto diferente. Una ventaja complementaria, consecuencia de este principio, es que, cuando se requiere un cambio de un aspecto de una aplicación, probablemente encontremos todas las piezas de código relevantes ubicadas en la misma unidad. 6.4 Duplicación de código La duplicación de código, es decir, tener el mismo segmento de código en una aplicación más de una vez es una señal de mal diseño y debe ser evitada. Generalmente, la duplicación de código es un síntoma de mala cohesión. 6.5 Cómo hacer ampliaciones Ampliar el código de una aplicación sin un buen nivel de cohesión implica tener que realizar modificaciones en más sitios. 6.6 Acoplamiento Idealmente, cuando sólo se cambia la implementación de una clase, las restantes clases no debieran verse afectadas por el cambio. Este sería un caso de acoplamiento débil. En cambio un alto acoplamiento obliga a modificar varias clases. 6.6.1 Utilización de la encapsulación para reducir el acoplamiento El uso de campos públicos expuestos en la interfaz de una clase no solo aporta la información que contiene la clase, sino también cómo se almacena exactamente la información de esos campos. Esto rompe uno de los principios fundamentales del diseño de clases de buena calidad, la encapsulación. Una pauta para el encapsulamiento (ocultar la información de la implementación a ojos de otras clases) sugiere que solamente debe hacerse visible para el exterior la información acerca de lo que hace una clase, no la información acerca de cómo lo hace. Esto tiene una gran ventaja: si ninguna otra clase conoce cómo está almacenada nuestra información entonces podemos cambiar fácilmente la forma de almacenarla sin romper otras clases. Un adecuado encapsulamiento de las clases reduce el acoplamiento y por lo tanto lleva a un mejor diseño. Podemos obligar a esta separación entre lo que se hace y cómo se hace definiendo los campos como privados y utilizando un método selector para acceder a ellos. Así en cualquier momento se puede cambiar la representación interna de los datos sin que se tenga que modificar la interfaz, ni, por lo tanto, el resto de clases que la usen. Es importante notar que jamás podemos desacoplar completamente las clases en una aplicación, de lo contrario, no podrían interactuar entre ellos objetos de diferentes clases. Más bien tratamos de mantener un grado de acoplamiento tan bajo como sea posible. 3 6.7 Diseño dirigido por responsabilidad El encapsulamiento no es el único factor que influye en el grado de acoplamiento, otro aspecto se conoce como diseño dirigido por responsabilidades. El diseño dirigido por responsabilidades es el proceso de diseñar clases asignando responsabilidades bien definidas a cada una. Este proceso puede usarse para determinar las clases que deben implementar una parte de cierta función de una aplicación. Expresa la idea de que cada clase será responsable de manejar sus propios datos. Un buen diseño dirigido por responsabilidades influye en el grado de acoplamiento y por consiguiente, también influye en la facilidad con que una aplicación puede ser modificada o extendida. 6.8 Localidad de cambios Uno de los principales objetivos de un diseño de clases de buena calidad es la localidad de los cambios: las modificaciones en una clase debieran tener efectos mínimos sobre las otras clases. Queremos crear un diseño de clases que facilite las modificaciones posteriores haciendo que los efectos de un cambio sean locales. Idealmente, debería cambiarse una única clase para realizar una modificación. Algunas veces, es necesario cambiar varias clases, pero apuntamos a que el cambio afecte a la menor cantidad de clases posible. Además, los cambios que requieran las otras clases debieran ser obvios, fáciles de detectar y fáciles de llevar adelante. En los proyectos grandes, logramos este objetivo siguiendo las reglas de diseño de buena calidad tales como usar diseño dirigido por responsabilidades y apuntar a un acoplamiento débil y a una alta cohesión. 6.9 Acoplamiento implícito El uso de campos públicos es una práctica que probablemente crea un acoplamiento fuerte entre las clases. Con este acoplamiento fuerte, puede ser necesario hacer cambios en más de una clase para algo que podría ser una simple modificación. Los campos públicos deben evitarse. Sin embargo, existe aún una forma peor de acoplamiento: el acoplamiento implícito. El acoplamiento implícito es una situación en la que una clase depende de la información interna de otra pero esta dependencia no es inmediatamente obvia. El acoplamiento fuerte en el caso de los campos públicos no era bueno, pero por lo menos era obvio. Si cambiamos los campos públicos en una clase y nos olvidamos de otra, la aplicación no compilará más y el compilador indicará el problema. En los casos de acoplamiento implícito, el omitir un cambio necesario puede no ser detectado. Un buen diseño de clases evitará esta forma de acoplamiento siguiendo la regla de diseño dirigido por responsabilidades Si nos fijamos en el diagrama de clases de una aplicación, las flechas en el diagrama son un buen primer indicador del grado de intensidad del acoplamiento de un programa: cuantas más flechas, más acoplamiento. Como una aproximación a un buen diseño de clases podemos apuntar a crear diagramas con pocas flechas. 4 6.10 Planificación por adelantado Una característica de un buen diseñador de software es la habilidad de anticiparse a los acontecimientos. ¿Qué cosas pueden cambiar? ¿Qué podemos asumir con seguridad que permanecerá sin cambios durante la vida del programa? Encapsular toda la información de la interfaz de usuario en una sola clase o en un conjunto de clases claramente definido forma parte de un buen diseño. 6.11 Cohesión El principio de cohesión puede aplicarse a clases y a métodos: las clases deben mostrar un alto grado de cohesión y lo mismo ocurre con los métodos. 6.11.1 Cohesión de métodos Un método cohesivo es responsable de una y sólo una tarea bien definida. Es más fácil de comprender lo que hace un segmento de código y realizar modificaciones si se usan métodos breves y bien cohesionados. Una estructura de métodos bien elegida, todos los métodos son relativamente cortos y fáciles de entender y sus nombres indican sus propósitos de forma bastante clara. Estas características representan una ayuda valiosa para un programador de mantenimiento. 6.11.2 Cohesión de clases La regla de cohesión de clases establece que cada clase debe representar una única entidad bien definida en el dominio del problema. Una clase cohesiva representa una única entidad bien definida. 6.11.3 Cohesión para la legibilidad Hay varias maneras en que un diseño se ve beneficiado por la alta cohesión. Las dos más importantes son la legibilidad y la reusabilidad. Un programador de mantenimiento fácilmente reconocerá por dónde comenzar a leer el código si necesita realizar un cambio en clases bien cohesionadas. La cohesión de clases también incrementa la legibilidad de un programa. 6.11.4 Cohesión para la reusabilidad La segunda gran ventaja de la cohesión es el alto potencial para la reutilización. La reusabilidad es otro aspecto importante de los métodos cohesivos. Las tareas separadas pueden reutilizarse más fácilmente y es debido al alto grado de cohesión. 6.12 Refactorización La refactorización es la actividad de reestructurar un diseño existente para mantener un buen diseño de clases cuando se modifica o se extiende una aplicación. Cuando diseñamos aplicaciones, debemos tratar de planificar por adelantado, anticipar los posibles cambios que podrían ser deseables en el futuro y crear clases altamente cohesivas y débilmente acopladas que faciliten las modificaciones. Está claro que no siempre podemos anticipar todas las futuras adaptaciones y que no es factible preparar un diseño que contemple todas las posibles ampliaciones que pensamos. 5 Este es el motivo por el que resulta importante la refactorización. Es frecuente que, durante el tiempo de vida de una aplicación, se le vaya agregando funcionalidad. Un efecto común que se produce de manera colateral es el lento crecimiento de la longitud de los métodos y de las clases. Añadir código en reiteradas ocasiones suele tener como consecuencia la disminución del grado de cohesión. Es muy probable que si se añade más y más código a un método o a una clase, llegue un momento en el que representará más de una tarea o una entidad claramente definida. La refactorización consiste justamente en repensar y rediseñar las estructuras de las clases y de los métodos. El efecto más común es que las clases se dividan en dos o que los métodos se dividan en dos o más métodos. La refactorización también incluye la unión de clases o de métodos que da por resultado una sola clase o un solo método, pero este caso es menos frecuente. 6.12.1 Refactorización y prueba Cuando algo se modifica existe la posibilidad de que se introduzcan errores, por lo tanto, es importante proceder cautelosamente, y antes de llevar a cabo la refactorización debemos asegurarnos de que exista un conjunto de pruebas para la versión actual del programa. Si las pruebas no existen, es prioritario crear algunas pruebas que se adecuen para implementar pruebas regresivas sobre la versión rediseñada. La refactorización debe comenzar sólo cuando existen las pruebas. Idealmente, la refactorización debe seguir dos pasos: La primera etapa consiste en refactorizar para mejorar la estructura interna del código, pero sin realizar ningún cambio en la funcionalidad de la aplicación. En otras palabras, al ejecutar el programa debería comportarse exactamente igual que antes. Una vez que este paso está completo, se deben ejecutar las pruebas regresivas para asegurarse de que no se hayan introducido errores no deseados. La segunda etapa comenzará una vez que se ha restablecido la funcionalidad base en la versión refactorizada. En ese momento estamos en una posición segura como para mejorar el programa. Una vez que se ha finalizado con la refactorización, por supuesto que será necesario ejecutar las pruebas en la nueva versión. La implementación de varios cambios al mismo tiempo (refactorizar y agregar nuevas características) hace que se vuelva más difícil localizar la fuente de los problemas, cuando estos ocurran. Una buena refactorización es tanto una manera de pensar como un conjunto de habilidades técnicas. Mientras realizamos cambios y extensiones en las aplicaciones, regularmente nos debemos preguntar si el diseño original aún representa la mejor solución. A medida que cambia la funcionalidad, también cambian los argumentos a favor o en contra sobre ciertos diseños. Lo que fue un buen diseño para una aplicación simple podría dejar de serlo cuando se agregan algunas extensiones. Reconocer estos cambios y realizar efectivamente estas modificaciones de refactorización en el código, generalmente ahorra una gran cantidad de tiempo y de esfuerzo al final. Cuanto antes limpiemos nuestro diseño, más trabajo ahorraremos. Debemos estar preparados para refactorizar métodos (convertir una secuencia de sentencias del cuerpo de un método existente en un método nuevo e independiente) y clases (tomar partes de una clase y crear una nueva clase a partir de ella). Considerar regularmente la refactorización mantiene nuestro diseño de clases limpio y finalmente, nos ahorra trabajo. 6 6.13 Refactorización para la independencia respecto del idioma Si queremos que el programa sea independiente del idioma, la situación ideal sería que el texto real de las palabras se almacene en un único lugar del código y que en todas las restantes partes se haga referencia a ellas de manera independiente del idioma. Una característica del lenguaje de programación que hace que esto sea posible son los tipos enumerados o enums. 6.13.1 Tipos Enumerados En su forma más simple, una definición de un tipo enumerado consiste en una envoltura exterior que utiliza la palabra enum en lugar de la palabra class, y un cuerpo que es simplemente una lista de nombres de variables que denotan el conjunto de valores que pertenece a este tipo. Por convenio, los nombres de estas variables se escriben en mayúsculas. Nunca creamos objetos de un tipo enumerado. Cada nombre dentro de la definición del tipo representa una única instancia de un tipo enumerado que ya se ha creado para usarla. public enum <NombreEnum>{ ENUM_OPC1, ENUM_OPC2, ENUM_OPC3, ENUM_OPC4; } Cada opción representa una instancia, a las que nos referimos de la siguiente manera: NombreEnum.ENUM_OPC1, NombreEnum.ENUM_OPC2, etc. A pesar de la simplicidad de su definición, los valores del tipo enumerado son objetos propiamente dichos, por lo tanto, no son iguales que los enteros. Java permite que las definiciones de los tipos enumerados contengan mucho más que una lista de valores de tipos: public enum <NombreEnum>{ ENUM_OPC1(“valor1”),ENUM_OPC2(“valor2”),ENUM_OPC3(“valor3”),ENUM_OPC4(“valor4”); private String valor; NombreEnum(String valor){ this.valor = valor; } public String getValor(){ return this.valor; } } En la declaración de cada tipo del enum se añade un parámetro. La definición del tipo incluye un constructor que no tiene la palabra public en su encabezado. Los constructores de los tipos enumerados nunca son públicos porque no podemos crear instancias de ellos. El parámetro asociado a cada valor se pasa mediante el parámetro del constructor. La definición del tipo incluye un campo, valor. El constructor almacena la cadena pasada como parámetro en este campo. Cada tipo enumerado define un método values que devuelve un array que contiene todos los valores del tipo. NombreEnum.values(. 7 Una instrucción switch selecciona una secuencia de instrucciones para su ejecución a partir de múltiples opciones diferentes. 6.13.2 Desacoplamiento adicional de la interfaz comandos Java permite que las definiciones de los tipos enumerados contengan mucho más que una lista de valores de tipos. 6.14 Directrices de diseño Antes de empezar a programar debemos plantearnos la longitud que tiene que tener un método y una clase. Un método es demasiado largo si hace más de una tarea lógica. Una clase es demasiado compleja si representa más de una entidad lógica. El tener estas pautas en mente puede mejorar significativamente su diseño de clases y permitirle resolver problemas más complejos y escribir programas mejores y más interesantes. 6.15 Ejecutar un programa fuera de BlueJ Para ejecutar un programa sin el entorno BlueJ necesitamos una cosa más: los métodos de clase que en Java se conocen también como métodos estáticos. 6.15.1 Métodos de clase Los métodos de clase que en Java se conocen también como métodos estáticos. Hasta ahora, todos los métodos que hemos visto han sido métodos de instancia: se invocan sobre una instancia de una clase. Lo que distingue a los métodos de clase de los métodos de instancia es que los métodos de clase pueden ser invocados sin tener una instancia, alcanza con tener la clase. Los métodos de clase están relacionados conceptualmente y usan una sintaxis relacionada con las variables de clase (la palabra clave en Java es static). Así como las variables de clase pertenecen a la clase antes que a una instancia, lo mismo ocurre con los métodos de clase. Un método de clase se define agregando la palabra clave static antes del nombre del tipo en la signatura del método: public static int getNumeroDeDiasDeEsteMes() { } Estos métodos pueden ser invocados utilizando la notación usual de punto, especificando el nombre de la clase en que está definido seguido del punto y luego del nombre del método. Si, por ejemplo, el método anterior está declarado en una clase de nombre Calendario, la siguiente sentencia lo invoca: int dias = Calendario.getNumeroDeDiasDeEstemes(); 8 6.15.2 El método main Si queremos iniciar una aplicación Java fuera del entorno BlueJ necesitamos usar un método de clase. En BlueJ, típicamente creamos un objeto e invocamos uno de sus métodos, pero fuera de este entorno una aplicación comienza sin que exista ningún objeto. Las clases son las únicas cosas que tenemos inicialmente, por lo que el primer método que será invocado debe ser un método de clase. La definición de Java para iniciar aplicaciones es bastante simple: el usuario especifica la clase que será iniciada y el sistema Java luego invocará un método denominado main ubicado dentro de dicha clase. Este método debe tener una signatura específica. Si no existe tal método en esa clase se informa un error. En el Apéndice E se describen los detalles de este método y los comandos necesarios para iniciar el sistema Java fuera del entorno BlueJ. 6.15.3 Limitaciones de los métodos de clase Dado que los métodos de clase están asociados con una clase antes que con una instancia, tienen dos limitaciones importantes. La primera limitación es que un método de clase no podrá acceder a ningún campo de instancia definido en la clase. Esto es lógico ya que los campos de instancia están asociados con objetos individuales. En cambio, los métodos de clase tienen el acceso restringido a las variables de clase de sus propias clases. La segunda limitación es como la primera: un método de clase no puede invocar a un método de instancia de la clase. Un método de clase sólo puede invocar a otros métodos de clase definidos en su propia clase. 9 6 Diseño de Clases .............................................................................................................................. 2 6.1 Introducción ............................................................................................................................... 2 6.3 Introducción al acoplamiento y a la cohesión............................................................................ 2 6.4 Duplicación de código ............................................................................................................... 3 6.5 Cómo hacer ampliaciones .......................................................................................................... 3 6.6 Acoplamiento ............................................................................................................................. 3 6.6.1 Utilización de la encapsulación para reducir el acoplamiento ........................................... 3 6.7 Diseño dirigido por responsabilidad .......................................................................................... 4 6.8 Localidad de cambios ................................................................................................................ 4 6.9 Acoplamiento implícito ............................................................................................................. 4 6.10 Planificación por adelantado...................................................................................................... 5 6.11 Cohesión .................................................................................................................................... 5 6.11.1 Cohesión de métodos.......................................................................................................... 5 6.11.2 Cohesión de clases.............................................................................................................. 5 6.11.3 Cohesión para la legibilidad ............................................................................................... 5 6.11.4 Cohesión para la reusabilidad............................................................................................. 5 6.12 Refactorización .......................................................................................................................... 5 6.12.1 Refactorización y prueba .................................................................................................... 6 6.13 Refactorización para la independencia respecto del idioma ...................................................... 7 6.13.1 Tipos Enumerados .............................................................................................................. 7 6.13.2 Desacoplamiento adicional de la interfaz comandos ......................................................... 8 6.14 Directrices de diseño.................................................................................................................. 8 6.15 Ejecutar un programa fuera de BlueJ......................................................................................... 8 6.15.1 Métodos de clase ................................................................................................................ 8 6.15.2 El método main ................................................................................................................. 9 6.15.3 Limitaciones de los métodos de clase ................................................................................ 9 10 Capítulo 7 Objetos conunbuen Comportamiento Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 7 Objetosconunbuencomportamiento 7.1 Introducción Los problemas que se presentan al escribir un programa cambiarán a lo largo del tiempo. Los errores de sintácticos son errores en la estructura del código fuente. Son fáciles de solucionar porque el compilador los señala y muestra algún mensaje de error. Los programadores más experimentados, que se enfrentan con problemas más complicados, generalmente tienen menos dificultad con la sintaxis del lenguaje y se concentran más en los errores lógicos. Un error lógico ocurre cuando el programa compila y se ejecuta sin errores obvios pero da resultados incorrectos. Los problemas de lógicos son mucho más severos y difíciles de encontrar que los errores de sintaxis. Es esencial para un ingeniero de software competente aprender la forma de manejar la exactitud y los caminos para reducir el número de errores existentes en una clase. Las pruebas son la actividad cuyo objetivo es determinar si un fragmento de código, un método, una clase o un programa, produce el comportamiento deseado. La depuración es el intento de localizar y corregir el origen de un error. La depuración viene a continuación de las pruebas. Si las pruebas demostraron que se presentó un error, usamos técnicas de depuración para encontrar exactamente dónde está ese error y corregirlo. Puede haber una cantidad significativa de trabajo entre saber que existe un error y encontrar su causa y solucionarlo. La escritura de programas para su mantenibilidad es tal vez el tema más fundamental. Se trata de escribir código de tal manera que, en primer término, se eviten los errores, y si aun así aparecen, puedan ser encontrados lo más fácilmente posible. Esto está fuertemente relacionado con el estilo de código y los comentarios, como también lo son los principios de calidad del código que comentamos en temas anteriores. El código debería ser fácil de entender, para que el programador original evite introducir errores y el programador de mantenimiento pueda localizar los posibles errores fácilmente. 7.2 Pruebas y depuración La prueba y la depuración son habilidades cruciales en el desarrollo de software. Frecuentemente necesitará controlar sus programas para ver si tienen errores y luego, cuando ocurran, localizarlos en el código. 7.3 Pruebas de unidades dentro de Bluej El término prueba de unidades se refiere a la prueba de partes individuales de una aplicación en contraposición con el término prueba de aplicación que es la prueba de una aplicación en su totalidad. Las unidades que se prueban pueden ser de tamaños diversos: Puede ser un grupo de clases Una sola clase Un método. 2 Debemos observar que la prueba de unidad puede tener lugar mucho antes de que una aplicación esté completa. Puede probarse cualquier método, una vez que esté escrito y compilado La experimentación y prueba temprana conlleva varios beneficios: En primer lugar, nos dan una experiencia valiosa con un sistema que hace posible localizar problemas tempranamente para corregirlos, a un costo mucho menor que si se hubieran encontrado en una etapa más avanzada del desarrollo. En segundo término, podemos comenzar por construir una serie de casos de prueba y resultados que pueden usarse una y otra vez a medida que el sistema crece. Cada vez que hacemos un cambio en un sistema, estas pruebas nos permiten controlar que no hayamos introducido errores inadvertidamente en el resto del sistema como resultado de las modificaciones. 7.3.1 Utilización de inspectores A la hora de probar interactivamente, la utilización de inspectores de objetos suele ser muy útil. Un componente esencial de la prueba de clases que usan estructuras de datos, es controlar que se comporten adecuadamente tanto cuando las estructuras están vacías como cuando están llenas. Una característica clave de una buena prueba consiste en asegurarse de controlar los límites dado que son, con gran frecuencia, los lugares en los que las cosas funcionan mal. 7.3.2 Pruebas positivas y pruebas negativas En una aplicación, cuando tenemos que decidir qué parte probar, generalmente distinguimos los casos de pruebas positivas de los casos de pruebas negativas. Una prueba positiva es la prueba de aquellos casos que esperamos que resulten exitosos. Cuando probamos con casos positivos nos tenemos que convencer de que el código realmente funciona como esperábamos. Una prueba negativa es la prueba de aquellos casos que esperamos que fallen. Cuando probamos con casos negativos esperamos que el programa maneje este error de cierta manera especificada y controlada. 7.4 Automatización de Pruebas Existen técnicas disponibles que nos permiten automatizar las pruebas repetitivas y así eliminar el trabajo pesado asociado que traen aparejadas. 7.4.1 Prueba de regresión Cuando se soluciona un error en un lugar determinado se puede, al mismo tiempo, introducir un nuevo error. Es deseable ejecutar pruebas de regresión cada vez que se realiza una modificación en el software. Las pruebas de regresión consisten en ejecutar nuevamente las pruebas pasadas previamente para asegurarse de que la nueva versión aún las supera. Probablemente, estas pruebas son mucho más realizables cuando se las puede automatizar de alguna manera. Una de las formas más fáciles de automatizar las pruebas de regresión es escribir un programa que actúa como un marco de pruebas o una batería de pruebas. 3 7.4.2 Pruebas automatizadas mediante JUnit Las clases de prueba son una característica de BlueJ y están diseñadas para implementar pruebas de regresión. Se basan en el marco de trabajo para pruebas JUnit. Utilizando las clases de prueba podemos automatizar las pruebas de regresión. La clase de prueba contiene el código para llevar a cabo una serie de pruebas preparadas y comprobar sus resultados. Esto hace que repetir las mismas pruebas sea mucho más sencillo. Las clases de prueba, en cierto sentido, son claramente diferentes de las clases ordinarias. El código de la clase de prueba puede ser escrito por una persona, pero también puede ser generado automáticamente por BlueJ. Cada clase de prueba suele contener pruebas para verificar la funcionalidad de su clase de referencia. La clase de prueba contiene tanto código fuente para ejecutar pruebas sobre una clase de referencia, como para comprobar si las pruebas han tenido éxito o no. 7.4.3 Grabación de una prueba BlueJ posibilita combinar la efectividad de las pruebas manuales con el poder de las pruebas automatizadas habilitándonos para grabar las pruebas manuales y luego ejecutarlas, con el fin de aplicar pruebas de regresión. Una aserción es una expresión que establece una condición que esperamos que resulte verdadera. Si la condición es falsa, decimos que falló esta aserción. Esto indica que hay un error en nuestro programa. 7.4.4 Bancos de pruebas Un banco de pruebas es un conjunto de objetos con un estado definido que sirve como base para las pruebas de unidades. Un objeto o grupo de objetos que se usa en una o más pruebas se conoce como banco de pruebas o fixture. En BlueJ se usa Test Fixture to Object Bench, para crear automáticamente los objetos de prueba a partir del código o Object Bench lo Test Fixture, para añadir al código los objetos de prueba. Una vez que hemos asociado un banco de pruebas con una clase de prueba, grabar las pruebas resulta bastante sencillo. Cada vez que creemos un nuevo método de prueba, los objetos del banco de pruebas aparecerán automáticamente en el banco de objetos, ya no habrá necesidad de crear nuevos objetos de prueba manualmente cada vez. La automatización de pruebas es un concepto poderoso porque hace más probable que las pruebas se escriban en primer lugar y más probable que se ejecuten y reejecuten a medida que el programa se desarrolle. Podría formarse el hábito de comenzar por escribir pruebas de unidad tempranamente en el desarrollo de un proyecto y mantenerlas actualizadas a medida que el proyecto avance. 4 7 Objetos con un buen comportamiento .............................................................................................. 2 7.1 Introducción ............................................................................................................................... 2 7.2 Pruebas y depuración ................................................................................................................. 2 7.3 Pruebas de unidades dentro de Bluej ......................................................................................... 2 7.3.1 Utilización de inspectores .................................................................................................. 3 7.3.2 Pruebas positivas y pruebas negativas................................................................................ 3 7.4 Automatización de Pruebas ....................................................................................................... 3 7.4.1 Prueba de regresión ............................................................................................................ 3 7.4.2 Pruebas automatizadas mediante JUnit .............................................................................. 4 7.4.3 Grabación de una prueba .................................................................................................... 4 7.4.4 Bancos de pruebas .............................................................................................................. 4 5 Capítulo 8 Mejoradela EstructuraMediante Herencia Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 8 Mejoradelaestructuramedianteherencia Los conceptos principales que usaremos para diseñar programas mejor estructurados son herencia y polimorfismo. Ambos conceptos son fundamentales en orientación a objetos y aparecen de distintas formas. 8.2 Utilización de la Herencia La herencia nos permite definir una clase como una ampliación de otra. Una superclase o clase padre, es una clase que es extendida por otra clase, o que otras clases heredan. Una subclase, clase hija, es una clase que extiende o amplia a otra clase. Hereda todos los campos y los métodos de la superclase. La herencia es un mecanismo que nos ofrece una solución al problema de duplicación de código. La característica esencial de esta técnica es que necesitamos describir las características comunes sólo una vez. La herencia también se denomina relación «es-un». La razón de esta nomenclatura radica en que la subclase es una especialización de la superclase. La herencia nos permite crear dos clases que son bastante similares evitando la necesidad de escribir dos veces la parte que es idéntica. 8.3 Jerarquías de herencia Más de una subclase puede heredar de la misma superclase y una subclase puede convertirse en la superclase de otras subclases. En consecuencia, las clases forman una jerarquía de herencia. Las clases que están vinculadas mediante una relación de herencia forman una jerarquía de herencia. La herencia es una técnica de abstracción que nos permite categorizar las clases de objetos bajo cierto criterio y nos ayuda a especificar las características de estas clases. 8.4 Herencia en Java La palabra clave extends define la relación de herencia. La frase «extends <SuperClase>» especifica que esta clase es una subclase de la clase SuperClase. public class <Nombre SubClase> extends <Nombre SuperClase> {…} La subclase define sólo aquellos campos que son únicos para los objetos de su tipo. Los campos de la superclase se heredan y no necesitan ser incluidos en el código de la subclase. 8.4.1 Herencia y derechos de acceso Los miembros definidos como públicos, ya sea en la superclase o en la subclase, serán accesibles para los objetos de otras clases, pero los miembros definidos como privados serán inaccesibles. La regla de privacidad también se aplica entre una subclase y su superclase, una subclase no puede acceder a los miembros privados de su superclase. Si un método de una subclase necesita acceder o modificar campos privados de su superclase, entonces la superclase necesitará ofrecer los métodos de selectores y/o mutadores apropiados. 2 Una subclase puede invocar a cualquier método público de su superclase como si fuera propio, no se necesita ninguna variable. 8.4.2 Herencia e inicialización Cuando creamos un objeto, el constructor de dicho objeto se encarga de inicializar todos los campos del objeto en algún estado razonable. En primer lugar, la superclase tendrá un constructor aun cuando no tengamos intención de crear, de manera directa, una instancia de la superclase. Este constructor recibe los parámetros necesarios para inicializar los campos de una instancia y contiene el código para llevar a cabo esta inicialización. El constructor de la subclase recibe los parámetros necesarios para inicializar tanto los campos propios como los de la superclase. El constructor de la subclase contendrá el siguiente código: super(<lista de parámetros>); La palabra clave super es, en realidad, una llamada al constructor de la superclase. El efecto de esta llamada es que se ejecuta el constructor de la supercase, formando parte de la ejecución del constructor de la subclase. Para que esta operación funcione, los parámetros necesarios para la inicialización de los campos de la superclase se pasan a su constructor como parámetros en la llamada a super. Constructor de superclase. El constructor de una subclase debe tener siempre como primera sentencia una invocación al constructor de su superclase. Si el código no incluye esta llamada, Java intentará insertarla automáticamente. La inserción automática de la llamada a la superclase sólo funciona si la superclase tiene un constructor sin parámetros; en el caso contrario, Java informa un error. En general, es una buena idea la de incluir siempre en los constructores llamadas explícitas a la superclase, aun cuando sea una llamada que el compilador puede generar automáticamente. Consideramos que esta inclusión forma parte de un buen estilo de programación, ya que evita la posibilidad de una mala interpretación y de confusión en el caso de que un lector no esté advertido de la generación automática de código. La herencia nos permite reutilizar en un nuevo contexto clases que fueron escritas previamente. El efecto de la reutilización es que se necesita una cantidad menor de código nuevo cuando introducimos elementos adicionales. Las clases que no se piensan usar para crear instancias, pero cuyo propósito es exclusivamente servir como superclases de otras clases se denominan clases abstractas. 3 8.6 Ventajas de la herencia (hasta ahora) Evita la duplicación de código. El uso de la herencia evita la necesidad de escribir copias de código idénticas o muy similares dos veces (o con frecuencia, aún más veces). Reutilización de código. El código que ya existe puede ser reutilizado. Si ya existe una clase similar a la que necesitamos, a veces podemos crear una subclase a partir de esa clase existente y reutilizar un poco de su código en lugar de implementar todo nuevamente. Facilita el mantenimiento. El mantenimiento de la aplicación se facilita pues la relación entre las clases está claramente expresada. Un cambio en un campo o en un método compartido entre diferentes tipos de subclases se realiza una sola vez. Ampliabilidad. El uso de la herencia hace mucho más fácil la extensión de una aplicación. 8.7 Subtipos Por analogía con la jerarquía de clases, los tipos forman una jerarquía de tipos. El tipo que se define mediante la definición de una subclase es un subtipo del tipo de su superclase. Hasta ahora, hemos interpretado el requerimiento de que los tipos de los parámetros “deben coincidir” como equivalente a decir que “deben ser del mismo tipo”: por ejemplo, que el nombre del tipo de un parámetro actual debe ser el mismo que el nombre del tipo del correspondiente parámetro formal. En realidad, esta es sólo una parte de la verdad porque los objetos de las subclases pueden usarse en cualquier lugar que se requiera el tipo de su superclase. 8.7.1 Subclases y subtipos Las clases definen tipos. Las clases pueden tener subclases, por lo tanto, los tipos definidos por las clases pueden tener subtipos. 8.7.2 Subtipos y asignaciones Variables y subtipos. Las variables pueden contener objetos del tipo declarado o de cualquier subtipo del tipo declarado. Una variable puede contener objetos del tipo declarado o de cualquier subtipo del tipo declarado. El tipo de una variable declara qué es lo que puede almacenar. La declaración de una variable de tipo <superclase> determina que esta variable puede referenciar instancias de tipo superclase. Pero como una <subclase> también tiene el subtipo <superclase>, es perfectamente legal almacenar una <subclase> en una variable que está pensada para almacenar <superclase>. Este principio se conoce como sustitución, Se pueden usar objetos de subtipos en cualquier lugar en el que se espera un objeto de un supertipo, ya que el objeto de la subclase es un caso especial de la superclase. Sin embargo hay que tener en cuenta que a una variable de tipo <subclase> no se le puede asignar un tipo <superclase>, ni una <subclase> diferente. 8.7.3 Subtipos y paso de parámetros El paso de parámetros, es decir, asignar un parámetro real a un parámetro formal se comporta exactamente de la misma manera que la asignación ordinaria a una variable. Este es el motivo por el que podemos pasar un objeto de tipo <subclase> al método que tiene un parámetro de tipo <superclase>. 4 8.7.4 Variables polimórficas En Java, las variables que contienen objetos son variables polimórficas. El término polimórfico se refiere al hecho de que una misma variable puede contener objetos de diferentes tipos del tipo declarado o de cualquier subtipo del tipo declarado. El polimorfismo aparece en los lenguajes orientados a objetos en numerosos contextos, las variables polimórficas constituyen justamente un primer ejemplo. La herencia evita la duplicación de código no sólo en las clases servidoras sino también en las clases clientes de aquellas. 8.7.5 Casting o proyección de tipos Algunas veces, la regla de que no puede asignarse un supertipo a un subtipo es más restrictiva de lo necesario: Vehicle v; Car c = new Car(); v = c; / / es correcta c = v; / / es un error Las instrucciones anteriores no compilarán. Obtendremos un error de compilación en la última línea porque no está permitida la asignación de una variable Vehicle en una variable Car. Sin embargo, si recorremos estas sentencias secuencialmente, sabemos que esta asignación podría realmente permitirse. Podemos ver que la variable v en realidad contiene un objeto de tipo Car, de modo que su asignación a la variable c debiera ser correcta. El compilador no es tan inteligente, traduce el código línea por línea, de modo que analiza la última línea aislada de las restantes, sin saber qué es lo que realmente se almacena en la variable v. Este problema se denomina pérdida de tipo. El tipo del objeto v realmente es un Coche, pero el compilador no lo sabe. Podemos resolver este problema diciendo explícitamente al sistema, que la variable v contiene un objeto Car, y lo hacemos utilizando el operador de enmascaramiento de tipos: c = (Car) v; / / correcto El operador de cast consiste en el nombre de un tipo (en este caso, Car) escrito entre paréntesis, que precede a una variable o a una expresión. Al usar esta operación, el compilador creerá que el objeto es un Car y no informará ningún error. Sin embargo, en tiempo de ejecución, el sistema Java verificará si realmente es un Car. Si fuimos cuidadosos, todo estará bien; si el objeto almacenado en v es de otro tipo, el sistema indicará un error en tiempo de ejecución (denominado ClassCastException) y el programa se detendrá. El casting solo se puede hacer con variables que constituyen una relación subtipo/supertipo, sino el compilador produce un error. El casting debiera evitarse siempre que sea posible, porque puede llevar a errores en tiempo de ejecución y esto es algo que claramente no queremos. El compilador no puede ayudarnos a asegurar la corrección de este caso. En la práctica, raramente se necesita del casting en un programa orientado a objetos bien estructurado. 5 En la mayoría de los casos, cuando se use un casting en el código, debiera reestructurarse el código para evitar el enmascaramiento, y se terminará con un programa mejor diseñado. 8.8 La clase Object Todas aquellas clases que no tienen una superclase explícita tienen como su superclase a la clase Object. Object es una clase de la biblioteca estándar de Java que sirve como superclase para todos los objetos. El compilador de Java inserta automáticamente la superclase Object en todas las clases que no tengan una declaración explícita extends por lo que jamás es necesario hacer esto manualmente. Todas las clases (con la única excepción de la clase Object en sí misma) heredan de Object, ya sea directa o indirectamente. El que todos los objetos tengan una superclase en común tiene dos propósitos. Primero, podemos declarar variables polimórficas de tipo Object que pueden contener cualquier objeto. En segundo lugar, la clase Object puede definir algunos métodos que están automáticamente disponibles para cada objeto existente. 8.9 Autoboxing y clases envoltorio Los tipos primitivos tales como int, boolean y char están separados de los tipos objeto. Sus valores no son instancias de clases y no derivan de la clase Object. Debido a esto, no son suptipos de Object y normalmente, no es posible ubicarlos dentro de una colección. Este es un inconveniente pues existen situaciones en las que quisiéramos crear, por ejemplo, una lista de valores int o un conjunto de valores char. ¿Qué podemos hacer? La solución de Java para este problema son las clases envoltorio. Cada tipo simple o primitivo tiene su correspondiente clase envoltorio que representa el mismo tipo pero que, en realidad, es un tipo objeto. Por ejemplo, la clase envoltorio para el tipo simple int es la clase de nombre Integer. El almacenamiento de valores primitivos en un objeto colección se lleva a cabo aún más fácilmente mediante una característica del compilador conocida como autoboxing. En cualquier lugar en el que se use un valor de un tipo primitivo en un contexto que requiere un tipo objeto, el compilador automáticamente envuelve al valor de tipo primitivo en un objeto con el envoltorio adecuado. Esto quiere decir que los valores de tipos primitivos se pueden agregar directamente a una colección. La operación inversa, unboxing, también se lleva a cabo automáticamente, es decir asignar a una variable primitiva un objeto de la clase envoltorio correspondiente. El proceso de autoboxing se aplica en cualquier lugar en el que se pase como parámetro un tipo primitivo a un método que espera un tipo envoltorio, y cuando un valor primitivo se almacena en una variable de su correspondiente tipo envoltorio. De manera similar, el proceso de unboxing se aplica cuando un valor de tipo envoltorio se pasa como parámetro a un método que espera un valor de tipo primitivo, y cuando se almacena en una variable de tipo primitivo. 6 8 Mejora de la estructura mediante herencia ....................................................................................... 2 8.2 Utilización de la Herencia ......................................................................................................... 2 8.3 Jerarquías de herencia ................................................................................................................ 2 8.4 Herencia en Java ........................................................................................................................ 2 8.4.1 Herencia y derechos de acceso ........................................................................................... 2 8.4.2 Herencia e inicialización .................................................................................................... 3 8.6 Ventajas de la herencia (hasta ahora) ........................................................................................ 4 8.7 Subtipos ..................................................................................................................................... 4 8.7.1 Subclases y subtipos ........................................................................................................... 4 8.7.2 Subtipos y asignaciones...................................................................................................... 4 8.7.3 Subtipos y paso de parámetros ........................................................................................... 4 8.7.4 Variables polimórficas ....................................................................................................... 5 8.7.5 Casting o proyección de tipos ............................................................................................ 5 8.8 La clase Object ....................................................................................................................... 6 8.9 Autoboxing y clases envoltorio ................................................................................................. 6 7 Capítulo 9 Mássobre laHerencia Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 9 Mássobreherencia 9.2 Tipo estático y tipo dinámico Denominamos tipo estático al tipo declarado de una variable en el código fuente, la representación estática del programa. Denominamos tipo dinámico al tipo del objeto almacenado en una variable porque depende de su asignación en tiempo de ejecución, el comportamiento dinámico del programa. Si con una variable invocamos un método que el tipo dinámico tiene, pero el estático no, el compilador informa de un error porque cuando controla los tipos usa el tipo estático. El tipo dinámico se conoce, frecuentemente, sólo en tiempo de ejecución por lo que el compilador no tiene otra opción más que usar el tipo estático cuando quiere hacer alguna verificación de tipos en tiempo de compilación. 9.3 Sustitución de métodos Sobreescritura (redefinición). Una subclase puede sobrescribir la implementación de un método. Para hacerlo la subclase declara un método con la misma signatura que la superclase pero con un cuerpo diferente. El método que sobrescribe tiene precedencia cuando se invoca sobre objetos de la subclase. 9.4 Búsqueda dinámica del método El control de tipos que realiza el compilador es sobre el tipo estático, pero en tiempo de ejecución los métodos que se ejecutan son los que corresponden al tipo dinámico. Veamos con más detalle cómo se invocan los métodos. Este procedimiento se conoce como búsqueda de métodos, asociación de métodos o despacho de métodos. Suponga que tenemos un objeto de clase Photopost almacenado en una variable v1 cuyo tipo declarado es Photopost. La clase Photopost tiene un método display y no tiene declarada ninguna superclase. Esta es una situación muy simple que no involucra herencia ni polimorfismo. Luego, ejecutamos la instrucción: v1. display(); El método display se invoca siguiendo los siguientes pasos: 1. Se accede a la variable v1. 2. Se localiza el objeto almacenado en esa variable (siguiendo la referencia). 3. Se encuentra la clase del objeto (siguiendo la referencia "instancia de"). 4. Se localiza la implementación del método imprimir en la clase y se ejecuta. A continuación, vemos la búsqueda de un método cuando hay herencia. El escenario es similar al anterior, pero esta vez la clase Photopost tiene una superclase, Post, y el método display está definido sólo en la superclase. Ejecutamos la misma instrucción. 2 La invocación al método comienza de manera similar: se ejecutan nuevamente los pasos l al 3 del escenario anterior pero luego continúa de manera diferente: 4. No se encuentra ningún método imprimir en la clase display en la clase Photopost. 5. Puesto que no se encontró ningún método adecuado, se busca en la superclase un método que coincida. Si no se encuentra ningún método en la superclase, se busca en la siguiente superclase (si es que existe). Esta búsqueda continúa hacia arriba por toda la jerarquía de herencia de la clase hasta Object o hasta que se encuentre definitivamente un método. Hay que tener en cuenta que, en tiempo de ejecución, debe encontrarse definitivamente un método que coincida, de lo contrario la clase no compila. 6. En nuestro ejemplo, el método display es encontrado en la clase Post y se ejecuta. Este escenario ilustra la manera en que los objetos heredan los métodos. Cualquier método que se encuentre en la superclase puede ser invocado sobre un objeto de la subclase y será correctamente encontrado y ejecutado. Ahora llegamos al escenario más interesante: la búsqueda de métodos con una variable polimórfica y un método sobrescrito. El escenario nuevamente es similar al anterior pero existen dos cambios: El tipo declarado de la variable v1 ahora es Post, no Photopost. El método display está definido en la clase Post y redefinido (o sobrescrito) en la clase Photopost. Los pasos que se siguen para la ejecución del método son exactamente los mismos pasos 1 al 4 del primer escenario. Es importante hacer algunas observaciones: No se usa ninguna regla especial para la búsqueda del método en los casos en los que el tipo dinámico no sea igual al tipo estático. El comportamiento que observamos es un resultado de las reglas generales. El método que se encuentra primero y que se ejecuta está determinado por el tipo dinámico, no por el tipo estático. En otras palabras, el hecho de que el tipo declarado de la variable v1 ahora es Post no tiene ningún efecto. La instancia con la que estamos trabajando es de la clase Photopost, y esto es todo lo que cuenta. Los métodos sobrescritos en las subclases tienen precedencia sobre los métodos de las superclases. Dado que la búsqueda de método comienza en la clase dinámica de la instancia (al final de la jerarquía de herencia) la última redefinición de un método es la que se encuentra primero y la que se ejecuta. Cuando un método está sobrescrito, sólo se ejecuta la última versión (la más baja en la jerarquía de herencia). Las versiones del mismo método en cualquier superclase no se ejecutan automáticamente. 9.5 Llamada a super en métodos Para llamar al método sobrescrito de la superclase desde la subclase escribiremos: public void <NombreMetodo>(){ super.<NombreMetodo>(); …} 3 Hay tres detalles importantes: Al contrario que las llamadas a super en los constructores, el nombre del método de la superclase está explícitamente establecido. Una llamada a super en un método siempre tiene la forma: super.<nombre-del-método>(<parámetros>) Nuevamente, y en contra de la regla de las llamadas a super en los constructores, la llamada a super en los métodos puede ocurrir en cualquier lugar dentro de dicho método. No tiene por qué ocurrir en la primera instrucción. Al contrario que en las llamadas a super en los constructores, no se genera automáticamente ninguna llamada a super y tampoco se requiere ninguna llamada a super, es completamente opcional. De modo que el comportamiento por defecto presenta el efecto de un método de una subclase ocultando completamente (sobrescribiendo) la versión de la superclase del mismo método contenida en la superclase. En ausencia del mecanismo de sustitución de métodos, los miembros no privados de una superclase son directamente accesibles desde sus subclases sin necesidad de ninguna sintaxis especial. Solo es necesario hacer una llamada super cuando haga falta acceder a la versión existente en la superclase de un método sustituido. 9.6 Polimorfismo de métodos Método polimórfico. Las llamadas a métodos en Java son polimórficas. El mismo método puede invocar en diferentes momentos diferentes métodos dependiendo del tipo dinámico de la variable usada para hacer la invocación. 9.7 Métodos de Object: toString La superclase universal Object implementa algunos métodos que, por tanto, forman parte de todos los objetos. El más interesante de estos métodos es toString. El propósito del método toString es crear una representación de un objeto en forma de cadena de caracteres. Esto es útil para cualquier objeto que pueda ser representado textualmente en la interfaz de usuario pero también es de ayuda para todos los otros objetos; por ejemplo, los objetos pueden ser fácilmente impresos con fines de depuración de un programa. En Object el valor de retorno de toString muestra simplemente el nombre de la clase del objeto y un número que representa la dirección de memoria donde el objeto está almacenado Todo objeto en Java tiene un método toString que puede usarse para devolver un String de su representación. Normalmente, para que este método resulte útil los objetos deben sustituirlo por una implementación propia. Los métodos System.out.print y System.out.println son especiales con respecto a esto: si el parámetro de uno de estos métodos no es un objeto String, el método invoca automáticamente al método toString de dicho objeto. 4 9.8 Igualdad entre objetos: equals y hashCode La clase Object define dos métodos, equals y hashCode, que están estrechamente relacionados con la tarea de determinar la similitud de objetos. En ocasiones, lo que deseamos saber es si dos variables diferentes están haciendo referencia al mismo objeto. Esto es exactamente lo que sucede cuando se pasa una variable de objeto como parámetro de un método, sólo hay un único objeto, pero tanto la variable original como la variable del parámetro hacen referencia a él. Lo mismo sucede cuando se asigna una variable de objeto a otra. Estas situaciones dan lugar a lo que se conoce con el nombre de igualdad de referencias. La igualdad de referencia se comprueba utilizando el operador ==. La igualdad de referencia no tiene en cuenta en absoluto el contenido de los objetos a los que hace referencia, sino que se limita a comprobar si hay un único objeto al que están haciendo referencia dos variables distintas o dos objetos distintos. Definimos a continuación igualdad de contenidos. Lo que una comprobación de la igualdad de contenidos pregunta es si dos objetos son iguales internamente, es decir, si los estados internos de los dos objetos coinciden. La forma de comprobar la igualdad de contenidos entre dos objetos consiste en verificar si los valores de sus dos conjuntos de campos son iguales. Como el parámetro del método equals es de tipo Object, esa comprobación solo tiene sentido si estamos comprobando campos del mismo tipo. 9.9 Acceso protegido Los lenguajes orientados a objetos frecuentemente definen un nivel de acceso que está a medias entre la restricción completa del acceso privado y la total disponibilidad del acceso público. En Java este nivel de acceso se denomina acceso protegido. En Java este nivel de acceso se denomina acceso protegido y es provisto por la palabra clave protected como alternativa entre public y private. La declaración de un campo o un método como protegido permite su acceso directo desde las subclases directas o indirectas. El acceso protegido permite acceder a los campos o a los métodos dentro de una misma clase y desde todas las subclases, pero no desde las clases restantes. Aunque el acceso protegido puede aplicarse a cualquier miembro de una clase, suele reservarse para los métodos y los constructores. No es frecuente aplicarlo a los campos porque debilitaría la encapsulación. Siempre que sea posible, los campos mutables de las superclases deberían permanecer privados. Sin embargo, existen casos válidos ocasionales en los que es deseable el acceso directo desde una subclase. La herencia representa una forma mucho más cerrada de acoplamiento que una relación normal de cliente. La herencia vincula las clases de manera muy cercana y la modificación de la superclase puede romper fácilmente la subclase. Este punto debiera tenerse en consideración cuando se diseñan las clases y sus relaciones. 5 9.10 El operador instanceof Hay ocasiones en las que necesitamos averiguar el tipo dinámico de un objeto, en lugar de limitarnos a tratar con un supertipo compartido. Para estos casos Java proporciona el operador instanceof. Este operador comprueba si un objeto determinado, es directa o indirectamente, una instancia de una determinada clase. La utilización del operador instanceof suele ir seguida inmediatamente por un cast de la referencia a objeto, para transformarla en el tipo identificado. de la referencia a objeto, para transformarla en el tipo identificado. Modificadores de acceso java: public, private, protected Visibilidad Desde la misma clase Desde cualquier clase del mismo paquete Desde una subclase del mismo paquete Desde una subclase fuera del mismo paquete Desde cualquier clase fuera del paquete public protected default private SI SI SI SI SI SI SI NO SI SI SI NO SI SI Herencia NO NO SI NO NO NO 6 9 Más sobre herencia ........................................................................................................................... 2 9.2 Tipo estático y tipo dinámico .................................................................................................... 2 9.3 Sustitución de métodos .............................................................................................................. 2 9.4 Búsqueda dinámica del método ................................................................................................. 2 9.5 Llamada a super en métodos .................................................................................................. 3 9.6 Polimorfismo de métodos .......................................................................................................... 4 9.7 Métodos de Object: toString ............................................................................................ 4 9.8 Igualdad entre objetos: equals y hashCode ........................................................................ 5 9.9 Acceso protegido ....................................................................................................................... 5 9.10 El operador instanceof........................................................................................................ 6 7 Capítulo 12 Tratamiento deErrores Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 12 TratamientodeErrores Los errores lógicos de los programas son más difíciles de detectar que los errores sintácticos porque el compilador no los detecta. Los errores lógicos surgen por diversos motivos y en algunas situaciones pueden estar encubiertos: La solución de un problema puede estar implementada incorrectamente. Se puede haber solicitado a un objeto que haga algo que es incapaz de hacer. Se puede haber usado un objeto de maneras tales que no coinciden con las anticipadas por el diseñador de la clase, dejando al objeto en un estado inapropiado o inconsistente. Además, aun cuando un programa se pruebe exhaustivamente puede fallar debido a circunstancias que están más allá del control del programador. Veremos cómo anticiparse y responder a las posibles situaciones de error que pueden surgir durante la ejecución de un programa. Veremos algunas sugerencias sobre la manera de informar de los errores cuando éstos ocurren. También veremos una breve introducción sobre los procesos de entrada y salida de texto como una de las situaciones en la que pueden aparecer fácilmente errores durante el tratamiento de los archivos. 12.2 Programación defensiva 12.2.1 Interacción cliente-servidor Los implementadores pueden adoptar como mínimo dos puntos de vista posibles al diseñar e implementar una clase servidor: Pueden asumir que los objetos cliente sabrán lo que están haciendo y requerirán servicios sólo de una manera coherente y bien definida. Pueden asumir que el servidor operará en un ambiente esencialmente hostil, en el que se deben tomar todas las medidas posibles para prevenir que los objetos cliente usen el servidor incorrectamente. Estas visiones representan claramente dos extremos opuestos. En la práctica, la mayoría de las situaciones asumirán posiciones intermedias. Estos diferentes puntos de vista proporcionan una base muy útil para discutir asuntos del estilo: ¿Cuántas comprobaciones de las solicitudes del cliente deben realizar los métodos del servidor? ¿Cómo debe informar el servidor, de los errores producidos a sus clientes? ¿Cómo puede un cliente anticipar un fallo en una solicitud al servidor? ¿Cómo puede tratar un cliente el fallo de una solicitud? No es posible invocar un método sobre el valor null y el resultado de esta invocación es un error en tiempo de ejecución. BlueJ informa esta situación como un NullPointerException y resalta la sentencia que lo produjo. 2 12.2.2 Validar argumentos Un objeto servidor es más vulnerable cuando su constructor y sus métodos reciben los valores de los argumentos a través de sus parámetros. Los valores que se pasan a un constructor se utilizan para establecer el estado inicial de un objeto. Los valores que se pasan a un método se usarán para influir sobre el efecto general de la llamada al método y pueden cambiar el estado del objeto y el resultado que el método devuelve. Por lo tanto, es vital que un servidor sepa si puede confiar en que los valores de los argumentos son válidos o si necesita verificar su validez por sí mismo. 12.3 Generación de informes de error de servidor En lugar de simplemente programar alrededor del problema en el servidor y dejar el problema localizado allí, es una buena práctica hacer que el servidor realice algún esfuerzo para indicar que ha surgido un problema, ya sea del propio cliente o de un usuario humano o del programador. En este sentido, existe la posibilidad de que funcione bien un cliente escrito incorrectamente. ¿Cuál es la mejor manera de que un servidor informe de los problemas cuando éstos ocurren? No hay una sola respuesta a esta pregunta y generalmente, la respuesta más adecuada dependerá del contexto particular en el que se use el objeto servidor. En las siguientes secciones exploraremos un conjunto de opciones para informar errores mediante un servidor. 12.3.1 Notificación al usuario La manera más obvia en que un objeto puede tratar de responder cuando detecta algo erróneo es intentar notificar al usuario de la aplicación de alguna forma. Las principales opciones son imprimir un mensaje de error usando System.out o System.err o mostrar una ventana de mensaje de error. Los dos problemas principales que tiene este abordaje son los siguientes: Asumen que la aplicación será usada por un usuario humano que verá el mensaje de error. Hay muchas aplicaciones que corren de manera completamente independiente de un usuario humano, en las que un mensaje de error o una ventana de error será completamente pasada por alto. La computadora en la que se ejecuta la aplicación podría no tener ningún dispositivo visual conectado para mostrar estos mensajes de error. Aun cuando exista un humano que pueda ver el mensaje de error, es raro que dicho usuario esté en posición de hacer algo con respecto al problema. Imagine a un usuario de un cajero automático enfrentado a un NullPointerException . Solamente en aquellos casos en los que la acción directa del usuario conduzca al problema puede estar capacitado para tomar alguna medida correctiva adecuada. Los programas que imprimen mensajes de error inapropiados tienden más a confundir al. Por lo tanto, excepto en un muy limitado conjunto de circunstancias, la notificación al usuario no es, en general, una solución al problema del informe de errores. 3 12.3.2 Notificación al objeto cliente Un enfoque radicalmente diferente al que abordamos hasta ahora consiste en que el servidor ofrezca alguna indicación al objeto cliente de que algo anduvo mal. Hay dos maneras de hacer esto: Un servidor puede usar el valor de retorno de un método para devolver una bandera que indique si fue exitoso o si ocurrió un fallo en la llamada a dicho método. Un servidor puede lanzar una excepción desde el método servidor si algo anda mal. Esto introduce una nueva característica de Java que se encuentra también en otros lenguajes de programación. Ambas técnicas tienen el beneficio de asegurar que el programador del cliente tenga en cuenta que puede fallar una llamada a un método sobre otro objeto. Sin embargo, sólo la decisión de lanzar una excepción evita activamente que el programador del cliente ignore las consecuencias del fallo del método. Cuando un método servidor ya tenga un tipo de retorno distinto de void, para evitar efectivamente que se retorne un valor de diagnóstico de tipo boolean, todavía existe alguna forma de indicar que ha ocurrido un error mediante el tipo de retorno. Es común que los métodos que retoman referencias a objetos utilicen el valor null para indicar un fallo o un error. En los métodos que retornan valores de tipos primitivos, se suele devolver algún valor fuera de los límites válidos que cumple un rol similar: por ejemplo, el método indexOf de la clase String devuelve un valor negativo para indicar que falló en encontrar el carácter buscado. Claramente, este enfoque no se puede usar en aquellos lugares en los que todos los valores del tipo de retorno ya tienen significados válidos para el cliente. En tales casos, generalmente será necesario pasar a la técnica alternativa de lanzar una excepción que, de hecho, ofrece importantes ventajas. Para ayudar a apreciar estas ventajas, es valioso considerar dos cuestiones asociadas al uso de los valores de retorno como indicadores de fallo o de error: Desafortunadamente, no hay manera de requerir al cliente que controle el valor de retorno en relación a sus propiedades de diagnóstico. En consecuencia, un cliente podría fácilmente actuar como si nada hubiera ocurrido y luego terminar con un NullPointerException, o peor todavía, podría usar el valor de retorno de diagnóstico como si fuera un valor de retorno normal, creando un error lógico difícil de diagnosticar. En algunos casos, podríamos usar el valor de diagnóstico con dos propósitos muy diferentes. Un propósito es notificar al cliente si su petición fue exitosa o no. El otro es indicar que hubo algún error en su solicitud, como por ejemplo, que se pasó un valor incorrecto como argumento. En muchos casos, una solicitud no exitosa no representa un error lógico de programación sino que se hizo una solicitud incorrecta. Debemos esperar respuestas muy diferentes de un cliente en estos dos casos. No existe una manera satisfactoria y general de resolver este conflicto usando simplemente valores de retorno. Otro problema es que pasa con los errores en los constructores. 4 12.4 Principios del lanzamiento de excepciones El lanzamiento de una excepción es la manera más efectiva que tiene un objeto servidor para indicar que es incapaz de completar la solicitud del cliente. Una de las mayores ventajas que tiene esta técnica es que usa un valor especial de retorno que hace casi imposible que un cliente ignore el hecho de que se ha lanzado una excepción y continúe indiferente. El fracaso del cliente al manejar una excepción dará por resultado que la aplicación termine inmediatamente. Además, el mecanismo de la excepción es independiente del valor de retorno de un método y se puede usar en todos los métodos, sin importar su tipo de retorno. 12.4.1 Lanzar una excepción Una excepción es un objeto que representa los detalles de un fallo de un programa. Se lanza una excepción para indicar que ha ocurrido un fallo. Se lanza una excepción usando una instrucción throw dentro de un método. El lanzamiento de una excepción tiene dos etapas: Primero se crea un objeto Excepction utilizando la palabra clave new (en este caso un objeto IllegalArgumentException) y luego se lanza el objeto Exception usando la palabra clave throw. Estas dos etapas se combinan casi invariablemente en una única sentencia: throw new TipoDeExcepcion (" cadena opcional de diagnóstico"); Cuando se crea un objeto excepción, se puede pasar una cadena de diagnóstico a su constructor. Esta cadena estará disponible para el receptor de la excepción mediante el método de acceso getMessage y toString del objeto excepción. Si esa excepción no se trata, la cadena se muestra también al usuario y esto conduce a la terminación de programa. Se puede expandir la documentación de un método para que incluya los detalles de cualquier excepción que lance mediante la etiqueta @throws del documentador de java (javadoc). 12.4.2 Excepciones comprobadas y no comprobadas Un objeto excepción es siempre una instancia de una clase de una jerarquía de herencia especial. Podemos crear nuevos tipos de excepciones creando subclases en esta jerarquía. Hablando estrictamente, las clases de excepciones siempre son subclases de la clase Throwable que está definida en el paquete java.lang. Normalmente se definen y usan las clases de excepciones como subclases de la clase Exception, también definida en java.lang. La clase Exception es una de las dos subclases directas de Throwable; la otra es Error. Las subclases de Error se reservan, generalmente, para los errores en tiempo de ejecución antes que para los errores sobre los que el programador tiene control. 5 El paquete java.lang define varias clases de excepciones que se ven comúnmente como por ejemplo: NullPointerException IndexOutOfBoundsException ClassCastException. Java divide las clases de excepciones en dos categorías: Excepciones comprobadas. Excepciones no comprobadas. Todas las subclases de la clase estándar de Java RunTimeException son excepciones no comprobadas, todas las restantes subclases de Exception son excepciones comprobadas. La diferencia es ésta: Las excepciones comprobadas están pensadas para aquellos casos en los que el cliente debe esperar que una operación falle (por ejemplo: cuando grabamos en un disco, sabemos que el disco puede estar lleno). En estos casos, el cliente está obligado a comprobar si la operación fue exitosa. Las excepciones no comprobadas están pensadas para aquellos casos que no deben fallar en una operación normal; generalmente indican un error en el programa. Saber qué categoría de excepción conviene lanzar en una circunstancia en particular no es una ciencia exacta pero podemos ofrecer las siguientes sugerencias: Una regla a priori que se puede aplicar es usar excepciones no comprobadas en las situaciones que podrían producir un fallo del programa, ya que se sospecha de la existencia de un error lógico en el programa que le impedirá continuar funcionando. Se desprende que las excepciones comprobadas deben usarse cuando ocurrió un problema pero existe alguna posibilidad de que el cliente efectúe alguna recuperación. Un problema con esta política es que asume que el servidor es suficientemente consciente del contexto en el que se está usando como para ser capaz de determinar si es probable que la recuperación del cliente sea posible. Otra regla a priori es usar excepciones no comprobadas en aquellas situaciones que pueden ser razonablemente evitadas. Por ejemplo, el uso de un índice no válido para acceder a un array es el resultado de un error lógico de programación que es completamente evitable y el hecho de que la excepción ArraylndexOutOfBoundsExcepction no es comprobada encaja con este modelo. Se desprende que las excepciones comprobadas deben usarse para situaciones de fallos que están bajo el control del programador como por ejemplo, que un disco se llene cuando se intenta grabar un archivo. Las reglas formales de Java que gobiernan el uso de las excepciones son significativamente diferentes para las excepciones comprobadas y para las no comprobadas. En términos simples, las reglas aseguran que un objeto cliente que llama a un método que puede disparar una excepción comprobada puede contener tanto código para anticipar la posibilidad de un problema como código para intentar manejar el problema cuando éste ocurra. 6 12.4.3 El efecto de una excepción ¿Qué ocurre cuando se lanza una excepción? Hay dos efectos a considerar: El efecto sobre el método en que se ha descubierto el problema y que ha lanzado la excepción. El efecto sobre aquel que ha invocado el método problemático. Cuando se lanza una excepción, la ejecución del método que la disparó termina inmediatamente, no continúa hasta el final del cuerpo del método. Una consecuencia particular de esto es que no se requiere un método con un tipo de retorno distinto de void para ejecutar una sentencia return en la ruta en que se lanza una excepción. Esto es razonable porque el lanzamiento de una excepción es una indicación de la incapacidad del método disparador para continuar con la ejecución normal, que incluye la imposibilidad de retornar un resultado válido. La ausencia de una instrucción de retorno en la ruta de ejecución termina generando una excepción es aceptable. En su lugar, el compilador indicará un error si se han escrito sentencias a continuación de la sentencia throw porque podrían no ejecutarse nunca. El efecto de una excepción en el sitio del programa que invocó al método es un poco más complejo. En particular, el efecto completo depende de si se ha escrito o no código para capturar la excepción. Lo que realmente ocurre a continuación de una excepción depende de si se captura o no. Si no se captura la excepción, el programa simplemente terminará con la indicación de que se ha lanzado una Exception sin capturar. 12.4.4 Utilización de excepciones no comprobadas Las excepciones no comprobadas son un tipo de excepción cuyo uso no requiere ninguna comprobación por parte del compilador. Las excepciones no comprobadas son las más fáciles de usar desde el punto de vista del programador, porque el compilador impone muy pocas reglas para su uso. Este es el sentido de "no comprobadas": el compilador no aplica ningún control especial sobre el método en el que se lanza una excepción no comprobada, ni tampoco en el lugar desde donde se invocó dicho método. Una clase Exception es no comprobada si es una subclase de la clase RuntimeException definida en el paquete java.lang. Hay muy poco para agregar sobre cómo lanzar una excepción no comprobada: simplemente usar una sentencia throw. Si seguimos también la convención de que las excepciones no comprobadas deben usarse en aquellas situaciones en las que esperamos que el resultado sea la terminación del programa, es decir, que no se va a capturar la excepción, entonces tampoco hay más para discutir sobre lo que debe hacer el método invocador puesto que no hará nada y dejará que el programa falle. Sin embargo, si existe la necesidad de capturar una excepción no comprobada, entonces se puede escribir un manejador de dicha excepción, exactamente de la misma manera que para una excepción comprobada. Una excepción no comprobada, que se usa comúnmente es IlegalArgumentException, es lanzada por un constructor o un método para indicar que los valores de sus argumentos no son los adecuados. 7 Es valioso tener un método que realice una serie de comprobaciones de validez de sus parámetros antes de proceder con el propósito principal del método. Esto hace menos probable que un método ejecute parte de sus acciones antes de lanzar una excepción debida a valores incorrectos en sus argumentos. Una razón particular para evitar esta situación es que la modificación parcial de un objeto probablemente lo deje en un estado inconsistente para su futuro uso. Si una operación falla por alguna razón, idealmente, el objeto deberá quedar en el estado en que estaba antes de que se intentara realizar la operación. 12.4.5 Como impedir la creación de un objeto Un uso importante de las excepciones es impedir que se creen objetos cuando no se los puede preparar con un estado inicial válido. Generalmente, este será el resultado del paso al constructor de valores de parámetro inapropiados. El proceso de lanzamiento de una excepción desde un constructor es exactamente el mismo que el lanzamiento desde un método. Una excepción que se lanza desde un constructor tiene el mismo efecto sobre el cliente que una excepción que se lanza desde un método. En consecuencia, el intento de crear un objeto con parámetros no válidos que provoquen que se lance una excepción fallará completamente; no dará por resultado que se almacene un valor null en la variable a la que se trataba de asignar el objeto. 12.5 Manejo de excepciones Los principios del lanzamiento de excepciones se aplican tanto para las excepciones comprobadas como para las no comprobadas, pero las reglas particulares de Java indican que el manejo de una excepción se convierte en un requerimiento sólo en el caso de excepciones comprobadas. Una clase de excepción comprobada es una subclase de Exception pero no de RuntimeException. Existen varias reglas que se deben seguir cuando se usan excepciones comprobadas porque el compilador obliga a tener controles tanto en los métodos que lanzan una excepción comprobada como en el invocador de dicho método. 12.5.1 Excepciones comprobadas: la cláusula throws Las excepciones comprobadas son un tipo de excepción cuyo uso requiere controles adicionales del compilador. En particular las excepciones comprobadas en Java requieren el uso de cláusulas throws y de sentencias try. El primer requerimiento del compilador es que un método que lanza una excepción comprobada debe declarar que lo hace mediante una cláusula throws añadida a la cabecera del método. Por ejemplo, un método que lanza una IOException comprobada del paquete java.io debe tener el siguiente encabezado: public void grabarEnArchivo (String archivoDestino) throws IOException Si bien se permite el uso de la cláusula throws para las excepciones no comprobadas, el compilador no lo requiere. Recomendamos que se use una cláusula throws solamente para enumerar las excepciones comprobadas que lanza un método. 8 Es importante distinguir entre la cláusula throws en el encabezado de un método y la etiqueta que se utiliza en el comentario que precede al método javadoc @throws. La última es completamente opcional para ambos tipos de excepción. 12.5.2 Anticipando las excepciones: la instrucción try El segundo requisito es que el invocador de un método que lanza una excepción comprobada debe proveer un tratamiento para dicha excepción. Esto generalmente implica escribir una rutina de tratamiento de excepciones bajo la forma de una instrucción try. Esta sentencia introduce dos nuevas palabras clave de Java, try y catch, que marcan un bloque try y un bloque catch respectivamente. try { Aquí se protege una o más instrucciones. } catch (Exception e) { Aquí se informa y se recupera de la excepción } El código de un programa que protege las instrucciones en las que podrían lanzar una excepción se denomina rutina de tratamiento de excepción. El código proporciona información y/o código para recuperarse del error. En un bloque try puede incluirse cualquier número de excepciones, colocaremos allí no solo la instrucción que puede fallar sino también todas aquellas otras instrucciones que estén relacionadas con ella de alguna manera. El bloque try representa una secuencia de acciones que queremos tratar como una sola unidad lógica, reconociendo que puede fallar en algún punto. El bloque catch intentará entonces tratar con la situación que se ha producido o informar acerca del problema, si es que se genera alguna excepción como consecuencia de la ejecución de cualquiera de las instrucciones contenidas dentro del bloque try asociado. Para comprender cómo funciona rutina de tratamiento de excepción es esencial comprender que una excepción impide que continúe en el llamante el flujo normal de control. Una excepción interrumpe la ejecución de las instrucciones del llamante, por lo que cualquier instrucción que esté inmediatamente a continuación de la instrucción que produjo el problema no se ejecutará. La pregunta que surge entonces es, ¿En qué punto se reanuda la ejecución en el llamante? La instrucción try proporciona la respuesta: si se genera una excepción debido a una instrucción invocada dentro del bloque try entonces la ejecución se reanuda en el correspondiente bloque catch. Las instrucciones ubicadas dentro de un bloque try se conocen como instrucciones protegidas. Si no surge ninguna excepción durante la ejecución de las sentencias protegidas, entonces se saltará al bloque catch cuando se llegue al final del bloque try. La ejecución continuará con cualquier instrucción que esté a continuación de la instrucción try/catch completa. El bloque catch indica el tipo de excepción que tiene designado tratar dentro de un par de paréntesis inmediatamente a continuación de la palabra catch. Así como el nombre del tipo de la excepción, también incluye un nombre de variable que tradicionalmente, es e o ex que se puede usar para hacer referencia al objeto Exception generado. 9 Una referencia a este objeto puede ser muy útil para proporcionar la información que se podrá usar para recuperarse del problema, o bien a la hora de informar de que ese problema se ha producido. Una vez que se completó el bloque catch, el control no retorna a la instrucción que causó la excepción. 12.5.3 Generación y captura de múltiples excepciones Algunas veces, un método lanza más de un tipo de excepción para indicar diferentes tipos de problemas. Cuando se trate de excepciones comprobadas deben enumerarse todas en la cláusula throws del método, separadas por comas. Por ejemplo: public void procesar ( ) throws EOException, FileNotFoundException Una rutina de tratamiento de excepciones debe capturar todas las excepciones comprobadas que se lanzan desde sus sentencias protegidas, de modo que una sentencia try puede contener varios bloques catch. Cuando se lanza una excepción mediante una llamada a método dentro de un bloque try, los bloques catch se evalúan en el orden en que están escritos hasta que se encuentra una coincidencia en el tipo de excepción. Una vez que se llega al final de un único bloque catch, la ejecución continúa debajo del último bloque catch. Si se desea, se puede usar polimorfismo para evitar la escritura de varios bloques catch. Sin embargo, esto puede tener como contrapartida no ser capaces de llevar a cabo acciones de recuperación específicas para cada tipo de excepción. Del proceso natural de coincidencias se desprende que es importante el orden de los bloques catch en una única sentencia try y que un bloque catch para un tipo de excepción en particular no puede estar debajo de uno de sus supertipos. El bloque del supertipo anterior siempre encontrará coincidencia antes que el bloque del subtipo que se controla. 12.5.5 Propagar una excepción Hasta ahora, hemos sugerido que una excepción debe ser capturada y tratada en la primera oportunidad disponible. Es decir, una excepción lanzada en un método process debe ser capturada y tratada en el método que haya llamado a process. En la realidad, este no es estrictamente el caso ya que Java permite que una excepción se propague desde el método receptor hasta su invocador y posiblemente, más allá. Un método propaga una excepción simplemente al no incluir una rutina de tratamiento de excepciones para proteger la instrucción que pueda lanzarla. Sin embargo, para una excepción comprobada, el compilador requiere que el método propagador incluya una cláusula throws aun cuando no lance en sí mismo una excepción. Si la excepción es no comprobada, la cláusula throws es opcional y preferimos omitirla. La propagación es común en los lugares en que el método invocador es incapaz de tomar una medida de recuperación o bien, no necesita ninguna, pero esto podría ser posible o necesario dentro de llamadas de nivel más alto. 10 12.5.6 La cláusula finally Una sentencia try puede incluir un tercer componente que es opcional: la cláusula final1y, que se omite con frecuencia. La cláusula finally se proporciona para instrucciones que se deben ejecutar tanto si se lanza una excepción para las sentencias protegidas como si no. Si el control alcanza el final del bloque try entonces se saltan los bloques cacth y se ejecuta la cláusula finally. Si se genera una excepción a dentro del bloque try, entonces se ejecuta el bloque catch apropiado y luego se sigue con la ejecución de la cláusula finally. try { //Bloque de sentencias que podrían generar una excepción. } catch (clase_de_excepcion_1 e){ //sentencias que se ejecutan si se ha producido una excepción de la clase clase_de_excepcion_1. } catch (clase_de_excepcion_2 e){ //sentencias que se ejecutan si se ha producido una excepción de la clase clase_de_excepcion_2. } catch (Exception e){ //sentencias que se ejecutan si se ha producido una excepción no capturada anteriormente. } finally { //Bloque de sentencias que se ejecutan siempre. } Se ejecuta una cláusula finally aunque se ejecute una sentencia return en los bloques try o catch. Si se genera una excepción en el bloque try pero no se captura, entonces también se ejecuta la cláusula finally. En el último caso, la excepción no capturada podría ser una excepción no comprobada que no requiere un bloque catch, por ejemplo. Sin embargo, también podría ser una excepción comprobada que no sea tratada mediante un bloque catch pero que se propaga desde ese método, para ser tratada a un nivel superior dentro de la pila de llamadas. En tal caso, la cláusula finally aún podría ser ejecutada. Es posible omitir los bloques catch en una instrucción try que disponga de un bloque try y una cláusula finally. 12.6 Definir nuevas clases de excepción Cuando las clases estándares de excepciones no describen satisfactoriamente la naturaleza del problema, se pueden definir nuevas clases más descriptivas usando el mecanismo de herencia. Las nuevas clases de excepciones comprobadas pueden definirse como subclases de una clase de excepción comprobada existente (tal como Exception) y las nuevas excepciones no comprobadas debieran ser subclases de la jerarquía RuntimeException. Todas las clases de excepción existentes soportan la inclusión de una cadena de diagnóstico que se pasa al constructor. Sin embargo, una de las principales razones para definir nuevas clases de excepción es la inclusión de más información dentro del objeto Exception para brindar el diagnóstico de error y de recuperación de errores. 11 El principio de incluir información que podría colaborar en la recuperación del error debe tenerse en cuenta particularmente cuando se definen nuevas clases de excepción comprobadas. La definición de los parámetros formales del constructor de una excepción ayudará a asegurar que la información de diagnóstico esté disponible. Además, cuando la recuperación no sea posible o no se intente, asegura que se sobrescriba el método toString de la excepción de modo que incluya la información adecuada y de esta manera, ayudará a diagnosticar el motivo del error. 12 12 Tratamiento de Errores ..................................................................................................................... 2 12.2 Programación defensiva............................................................................................................. 2 12.2.1 Interacción cliente-servidor ................................................................................................ 2 12.2.2 Validar argumentos ............................................................................................................ 3 12.3 Generación de informes de error de servidor ............................................................................ 3 12.3.1 Notificación al usuario ....................................................................................................... 3 12.3.2 Notificación al objeto cliente ............................................................................................. 4 12.4 Principios del lanzamiento de excepciones ............................................................................... 5 12.4.1 Lanzar una excepción ......................................................................................................... 5 12.4.2 Excepciones comprobadas y no comprobadas ................................................................... 5 12.4.3 El efecto de una excepción ................................................................................................. 7 12.4.4 Utilización de excepciones no comprobadas...................................................................... 7 12.4.5 Como impedir la creación de un objeto.............................................................................. 8 12.5 Manejo de excepciones .............................................................................................................. 8 12.5.1 Excepciones comprobadas: la cláusula throws ............................................................... 8 12.5.2 Anticipando las excepciones: la instrucción try ................................................................. 9 12.5.3 Generación y captura de múltiples excepciones............................................................... 10 12.5.5 Propagar una excepción.................................................................................................... 10 12.5.6 La cláusula finally ...................................................................................................... 11 12.6 Definir nuevas clases de excepción ......................................................................................... 11 13 Apéndice B Tiposde datosenJava Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 ApéndiceB‐TiposdedatosenJava Java reconoce dos categorías de tipos: Tipos primitivos. Tipos objeto. Los tipos primitivos se almacenan directamente en las variables y tienen valores semánticos (se copian los valores cuando se asignan a otra variable). Los tipos objeto se almacenan mediante referencias al objeto (no se almacena el objeto propiamente dicho); cuando se asignan a otra variable sólo se copia la referencia, no el objeto. B.1 Tipos primitivos La siguiente tabla enumera los tipos primitivos de Java: Tipo byte Descripción Entero de 1 byte de tamaño (8 bit) Long 1 byte Rango −128 a 127 short Entero corto (16 bit) 2 bytes −32768 a 32767 int Entero (32 bit) 4 bytes −231 a 231−1 long Entero largo (64 bit) 8 bytes −263 a 263−1 float Real Coma Flotante Simple Precisión 32 bytes ± 1,4 · 10-38 a ±3,4·1038 double Real Coma Flotante Doble Precisión 64 bytes ± 4,9·10-324 a ±1,8·10308 char Un solo carácter (16 bit) 2 bytes 0 a 65.535 1 bit true o false boolean Un valor lógico (verdadero o falso) Notas: Un número que no contiene un punto decimal se interpreta generalmente como un int, pero se convierte automáticamente a los tipos short, byte o long cuando se le asigna (si el valor encaja). Se puede declarar un literal como long añadiendo una ' L' al final del número (también se puede utilizar la letra '1' (L minúscula) pero debería evitarse ya que se puede confundir fácilmente con el uno). Un número con un punto decimal se considera de tipo double. Se puede especificar un literal como un float añadiendo una 'F' o 'f' al final del número. Un carácter se puede escribir como un carácter Unicode encerrándolo entre comillas simples o como un valor Unicode de cuatro dígitos precedidos por '\u'. Los dos literales booleanos son true y false. No existen métodos asociados con los tipos primitivos. Sin embargo, cuando se usa un tipo primitivo en un contexto que requiere un tipo objeto se puede usar el proceso de autoboxing para convertir un valor primitivo en su correspondiente objeto. 2 B.2 Conversión de tipos primitivos En ocasiones, es necesario convertir un valor de un tipo primitivo a un valor de otro tipo primitivo. Normalmente suele ser un valor de un tipo con un cierto rango de valores a otro con un rango más pequeño. Este proceso se denomina cast. La conversión de tipos implica casi siempre pérdida de información. Por ejemplo el paso de un tipo de coma flotante a tipo entero. No es posible convertir un valor de tipo boolean en ningún otro tipo con un cast, o viceversa. El operador cast consta del nombre de un tipo primitivo escrito entre paréntesis delante de una variable o expresión: int val = (int)mean; si mean es una variable de tipo double con valor 3.9, entonces la instrucción anterior almacenaría el valor entero 3 en la variable val. B.3 Tipos objeto Los tipos objeto se almacenan mediante referencias al objeto (no se almacena el objeto propiamente dicho); cuando se asignan a otra variable sólo se copia la referencia, no el objeto. Todos los tipos que no aparecen en la sección Tipos primitivos son tipos objeto. Esto incluye los tipos clase e interfaz de la biblioteca estándar de Java (como por ejemplo, String) y los tipos definidos por el usuario. Una variable de tipo objeto contiene una referencia (o un «puntero») a un objeto. Las asignaciones y los pasajes de parámetros utilizan referencias semánticas (es decir, se copia la referencia, no el objeto). Después de asignar una variable a otra, ambas variables hacen referencia al mismo objeto. Se dice que las dos variables son alias del mismo objeto. Las clases son las plantillas de los objetos: definen los campos y los métodos que poseerá cada instancia. Los arrays se comportan como tipos objeto; también utilizan referencias semánticas. No hay definición de clase para las matrices. 3 B.4 Clases envoltorio En Java, cada tipo primitivo tiene su correspondiente clase envoltorio que representa el mismo tipo pero que en realidad, es un tipo objeto. Estas clases hacen posible que se usen valores de tipos primitivos en los lugares en que se requieren tipos objeto mediante un proceso conocido como autoboxing. La siguiente tabla enumera los tipos primitivos y sus correspondientes clases envoltorio del paquete java.lang. Los nombres de las clases envoltorio coinciden con los nombres de los tipos primitivos, pero con su primera letra en mayúscula. Tipo de datos simple byte Tipo de envoltorio Byte Clase equivalente java.lang.Byte short Short java.lang.Short int Int java.lang.Integer long Long java.lang.Long float Float java.lang.Float double Double java.lang.Double char Char java.lang.Character boolean Boolean java.lang.Boolean Siempre que se use un valor de un tipo primitivo en un contexto que requiera un tipo objeto, el compilador utiliza la propiedad de autoboxing para encapsular automáticamente al valor de tipo primitivo en un objeto envoltorio equivalente. La operación inversa autounboxing también se lleva a cabo automáticamente cuando se utiliza un objeto envoltorio en un contexto que requiere un valor del tipo primitivo correspondiente. B.5 Cast de tipo de objeto. Puesto que un objeto puede pertenecer a una jerarquía de herencia de tipos, en ocasiones es necesario convertir una referencia de objeto en una referencia a un subtipo situado más abajo en la jerarquía de herencia. Este proceso se denomina casting o downcasting. El operador cast está compuesto por el nombre de una clase o tipo de interfaz escrito entre paréntesis delante de una variable o expresión: Car c = (Car)veh; Si el tipo declarado estático de la variable veh es Vehicle y Car es una subclase de Vehicle, entonces esta instrucción podrá compilarse correctamente. En tiempo de ejecución se hace otra comprobación independiente, para garantizar que el objeto al que hace referencia veh sea realmente un Car y no una instancia de un subtipo distinto. El cast entre tipo de objetos es completamente distinto de la conversión entre tipos primitivos. El cast entre tipos de objetos no implica ninguna modificación del objeto implicado. Se trata simplemente de una forma de obtener acceso a una información de tipo que ya cierta para ese objeto, es decir, que forma parte de su tipo dinámico completo. 4 Apéndice B - Tipos de datos en Java ....................................................................................................... 2 B.1 Tipos primitivos ......................................................................................................................... 2 B.2 Conversión de tipos primitivos .................................................................................................. 3 B.3 Tipos objeto ............................................................................................................................... 3 B.4 Clases envoltorio ....................................................................................................................... 4 B.5 Cast de tipo de objeto. ............................................................................................................... 4 5 Apéndice C Operadores Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 ApéndiceC‐Operadores C.1 Expresiones aritméticas Java dispone de una cantidad considerable de operadores para expresiones aritméticas y lógicas. La tabla C.1 muestra todo aquello que se clasifica como un operador, incluyendo la conversión de tipos (casting) y el pasaje de parámetros. Los principales operadores aritméticos son: Operador + Formato op1 + op2 Descripción Suma aritmética de dos operandos - op1 - op2 Resta aritmética de dos operandos - -op1 * op1 * op2 Multiplicación de dos operandos op1 / op2 División entera de dos operandos % op1 op2 Resto de la división entera o módulo ++ −− ++op1 op1++ --op1 op1-- Cambio de signo Incremento unitario Decremento unitario Tanto en la división como en el módulo, los resultados de las operaciones dependen de si sus operandos son enteros o si son valores de punto flotante. Entre dos valores enteros, la división retiene el resultado entero y descarta cualquier resto; pero entre dos valores de punto flotante, el resultado es un valor de punto flotante. Cuando en una operación aparecen más operadores, se deben usar las reglas de precedencia para indicar el orden de su aplicación. En la Tabla C.1, los operadores se presentan por nivel de precedencia, de mayor a menor. En la primera fila aparecen los operadores de nivel de precedencia más alto. Por ejemplo, podemos ver que la multiplicación, la división y el módulo preceden a la suma y a la resta. Los operadores binarios que tienen el mismo nivel de precedencia se evalúan de izquierda a derecha. Los operadores unarios que tienen el mismo nivel de precedencia se evalúan de derecha a izquierda. Se pueden usar paréntesis cuando se necesite alterar el orden de evaluación. Los principales operadores unarios son: −, !, ++, −−, [] Y NEW. Observe que algunos operadores aparecen en las dos primeras filas de la Tabla C.l. Los que aparecen en la primera fila admiten un solo operando a su izquierda. Los que están en la segunda fila admiten un solo operando a su derecha. 2 Precedencia de operadores en Java Operadores Precedencia [ ] . ++ -- (parametros) ++ -- + - ! ~ new cast Multiplicativos * / % Aditivos + De movimiento (shift) << >> >>> Relacionales < > <= >= instanceof De igualdad == != AND a nivel de bit (bitwise AND) & OR exclusivo a nivel de bit ^ OR inclusivo a nivel de bit | AND lógico && OR lógico || Ternarios ?: De asignación = += -= *= /= %= &= ^= |= <<= >>= >>>= C.2 Expresiones booleanas En las ex presiones lógicas, se usan los operadores para combinar operandos y producir un único valor lógico, ya sea verdadero o falso (true o fa/s e). Las expresiones lógicas generalmente se encuentran en las condiciones de las sentencias ife/se y en las de los ciclos. Los operadores relacionales o de comparación combinan generalmente un par de operandos aritméticos, aunque también se utilizan para evaluar la igualdad y la desigualdad de referencias a objetos. Los operadores relacionales de Java son: Operador Formato > op1 > op2 < op1 < op2 >= op1 >= op2 <= op1<= op2 == op1 == op2 != op1 != op2 Devuelve true Devuelve true Devuelve true Devuelve true Devuelve true Devuelve true Descripción (cierto) si op1 es mayor que op2 (cierto) si op1 es menor que op2 (cierto) si op1 es mayor o igual que op2 (cierto) si op1 es menor o igualque op2 (cierto) si op1 es igual a op2 (cierto) si op1 es distinto de op2 Los operadores lógicos binarios combinan dos expresiones lógicas para producir otro valor lógico. Los operadores son: Operador Formato Descripción && op1 && op2 Y lógico. Devuelve true si son ciertos op1 y op2 op1 || op2 O lógico. Devuelve true si son ciertos op1 o op2 ! !op1 Negación lógica. Devuelve true si es false op1. Y además, ! not Que toma una expresión lógica y cambia su valor de verdadero a falso y viceversa. 3 C.3 Operadores de cortocircuito Los operadores && y || se llaman operadores en cortocircuito porque si no se cumple la condición de un término no se evalúa el resto de la operación. C.4 Operadores de asignación Operador Formato Equivalencia += op1 += op2 op1 = op1 + op2 -= op1 -= op2 op1 = op1 − op2 *= op1 *= op2 op1 = op1 * op2 /= op1 /= op2 op1 = op1 / op2 %= op1 op2 op1 = op1 % op2 &= op1 op2 op1 = op1 & op2 |= op1 op2 op1 = op1 | op2 ^= op1 ^op2 op1 = op1 ^ op2 >>= op1 >>op2 op1 = op1 >> op2 <<= op1 op2 op1 = op1 << op2 >>>= op1 >>>op2 op1 = op1 >>> op2 C.5 Secuencias de escape Secuencia Significado \’ Comillas simples \” Dobles comillas \\ Contrabarra \b Retroceso \n Línea siguiente \f Form feed \r Retorno de carro \t Tabulador \a Alarma \xxx Carácter en octal \0 Carácter nulo \uxxxx Carácter en hexadecimal Unicode 4 Apéndice C - Operadores ......................................................................................................................... 2 C.1 Expresiones aritméticas ............................................................................................................. 2 C.2 Expresiones booleanas ............................................................................................................... 3 C.3 Operadores de cortocircuito ....................................................................................................... 4 C.4 Operadores de asignación .......................................................................................................... 4 C.5 Secuencias de escape ................................................................................................................. 4 5 Apéndice D Estructurasde ControlenJava Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 ApéndiceD‐EstructurasdeControlenJava D.1 Estructuras de Control Las estructuras de control afectan al orden en que se ejecutan las instrucciones. Existen dos categorías principales: Estructuras de selección. Estructuras de repetición. Una estructura de selección proporciona un punto de decisión en el que se realiza una elección para seguir una ruta a través del cuerpo de un método o constructor en lugar de otra ruta. Una estructura de repetición ofrece la opción de repetir instrucciones, un número definido o indefinido de veces. La repetición de un número definido de veces lo hacemos con el bucle for y el for-each. La repetición indefinida lo hacemos con el bucle while y do. D.2 Estructuras de selección D.2.1 if La sentencia if permite en un programa tomar la decisión sobre la ejecución o no de una acción o de un grupo de acciones, mediante la evaluación de una expresión lógica o booleana. La acción o grupo de acciones se ejecutan cuando la condición es cierta. En caso contrario no se ejecutan y se saltan. if (condición){ instrucciones } D.2.2 Sentencia if - else Esta clase de sentencia if ofrece dos alternativas a seguir, basadas en la comprobación de la condición. La palabra reservada else separa las instrucciones utilizadas para ejecutar cada alternativa. Si la evaluación de la condición es verdadera, se ejecuta la instrucción 1 o secuencia de instrucciones l, mientras que si la evaluación es falsa se ejecuta la instrucción 2 o secuencia de instrucciones 2. if (condición){ instrucciones 1 } else { instrucciones 2 } 2 D.2.3 switch Cuando se tienen muchas alternativas posibles a elegir, el uso de sentencias if, else-if puede resultar bastante complicado, siendo en general más adecuado en estos casos el empleo de la instrucción switch. La sintaxis de una instrucción switch es la siguiente: switch(expresión){ case valor1: instrucciones; break; case valor2: case valor3: instrucciones; break; ……… default: instrucciones; break; } La expresión, que es obligatorio que esté entre paréntesis, tiene que evaluarse a un entero, un carácter, un enumerado o un booleano. A continuación, en cada case aparece un valor que únicamente puede ser una expresión constante, es decir, una expresión cuyo valor se puede conocer antes de empezar a ejecutar el programa del mismo tipo que la expresión del switch. Después de cada case se puede poner una única sentencia o un conjunto de ellas. Los valores asociados en cada case se comparan en el orden en que están escritos. Cuando se quiere interrumpir la ejecución de sentencias se utiliza la sentencia break que hace que el control del programa termine el switch y continúe ejecutando la sentencia que se encuentre después de esta estructura. Si no coincide el valor de ningún case con el resultado de la expresión, se ejecuta la parte default. Si ningún valor de los case coincide con el resultado de la expresión y la parte default no existe, ya que es opcional, no se ejecuta nada de la estructura switch. 3 D.3 Estructuras de repetición D.3.1 While El bucle while ejecuta una sentencia o bloque de sentencias mientras se cumple una determinada condición. La condición tiene que estar obligatoriamente entre paréntesis. La condición es una expresión lógica. Si la condición vale true, se ejecutan las sentencias que componen el bucle. Cuando concluye la ejecución de las instrucciones del bucle se vuelve a evaluar la condición. De nuevo, si la condición es cierta se vuelven a ejecutar las instrucciones del bucle. En algún momento la condición valdrá false, en cuyo caso finaliza la ejecución del bucle y el programa continúa ejecutándose por la sentencia que se encuentre a continuación de la estructura while. Un problema frecuente en programación se produce cuando aparecen bucles infinitos. Un bucle infinito es aquel que nunca termina. Los bucles while infinitos se producen debido a que la condición que se comprueba nunca se hace falsa, de modo que el bucle while ejecuta repetidamente sus sentencias una y otra vez. while (condición){ instrucciones } D.3.2 do-while La sentencia do-while es similar a la sentencia while, excepto que la condición se comprueba después de que el bloque de sentencias se ejecute. La sentencia o sentencias se ejecutan y, a continuación, se evalúa la condición. Si la condición se evalúa a un valor verdadero, las sentencias se ejecutan de nuevo. Este proceso se repite hasta que expresión se evalúa a un valor falso, en cuyo momento se sale de la sentencia do-while. Dado que el test condicional se realiza al final del bucle la sentencia o bloque de sentencias se ejecuta al menos una vez. do{ instrucciones }while (condición); 4 D.3.3 for Tiene dos formas diferentes. El Bucle for-each, se utiliza exclusivamente para iterar a través de los elementos de una colección. A la variable del bucle se le asigna el valor de los elementos sucesivos de la colección en cada iteración del bucle. for (declaración-de-variables : colección){ instrucciones } El bucle for está diseñado para ejecutar una secuencia de sentencias un número fijo de veces. La sintaxis de la sentencia for es: for (inicialización ; condición ; incremento){ instrucciones } Las sentencias podrán ser cero, una única sentencia o un bloque, y serán lo que se repita durante el proceso del bucle. La inicialización fija los valores iniciales de la variable o variables de control antes de que el bucle for se procese y ejecute solo una vez. Si se desea inicializar más de un valor, se puede utilizar un operador especial de los bucles for en Java, el operador coma, para pegar sentencias. Cuando no se tiene que inicializar, se omite este apartado; sin embargo, nunca se debe omitir el punto y coma que actúa como separador. La condición de terminación se comprueba antes de cada iteración del bucle y éste se repite mientras que dicha condición se evalúe a un valor verdadero. Si se omite no se realiza ninguna prueba y se ejecuta siempre la sentencia for. El incremento se ejecuta después de que se ejecuten las sentencias y antes de que se realice la siguiente prueba de la condición de terminación. Normalmente esta parte se utiliza para incrementar o decrementar el valor de más variables de control y, al igual que en la inicialización, se puede usar en ella el operador coma para pegar sentencias. Cuando no se tienen valores a incrementar se puede suprimir este apartado. En esencia, el bucle for comprueba si la condición de terminación es verdadera Si la condición es Verdadera, se ejecutan las sentencias del interior del bucle, y si la condición es falsa, se saltan todas las sentencias del interior del bucle, es decir, no se ejecutan. Cuando la condición es verdadera, el bucle ejecuta una iteración, todas sus sentencias, y a continuación la variable de control del bucle se incrementa. 5 D.4 Excepciones El lanzamiento y la captura de excepciones proporciona otro par de construcciones que alteran el flujo del control. try { //Bloque de sentencias que podrían generar una excepción. } catch (clase_de_excepcion_1 e){ //sentencias que se ejecutan si se ha producido una excepción de la clase clase_de_excepcion_1. } catch (clase_de_excepcion_2 e){ //sentencias que se ejecutan si se ha producido una excepción de la clase clase_de_excepcion_2. } catch (Exception e){ //sentencias que se ejecutan si se ha producido una excepción no capturada anteriormente. } finally { //Bloque de sentencias que se ejecutan siempre. } Una sentencia de excepción puede tener cualquier número de cláusulas catch que son evaluadas en el orden en que aparecen y se ejecuta sólo la primera cláusula que coincida. Una cláusula coincide si el tipo dinámico del objeto excepción que ha sido lanzado es compatible en la asignación con el tipo de excepción declarado en la cláusula catch. La cláusula flnally es opcional. Se pueden tratar varias excepciones en la misma clausula catch escribiendo la lista de tipos de excepción, separados por el símbolo "|". try { ... var.doSomething(); ... } catch (EOFException | FileNotFoundException e){ ... } 6 La gestión automática de recursos, try con recursos, refleja el hecho de que las instrucciones try se utilizan a menudo para proteger instrucciones que utilizan recursos que hay que cerrar una vez que se haya terminado de utilizarlos, independientemente de si ese uso ha tenido éxito o ha fallado. La cabecera de la instrucción try se amplía para incluir la apertura del recurso, que a menudo es un archivo, y el recurso será cerrado automáticamente al final de la instrucción try. try (FileWriter writer = new FileWriter(filename)){ ... Utilizar el escritor... ... } catch (IOException e)¨ ... } 7 Apéndice D - Estructuras de Control en Java........................................................................................... 2 D.1 Estructuras de Control ............................................................................................................... 2 D.2 Estructuras de selección ............................................................................................................. 2 D.2.1 if.......................................................................................................................................... 2 D.2.2 Sentencia if - else ............................................................................................................... 2 D.2.3 switch.................................................................................................................................. 3 D.3 Estructuras de repetición............................................................................................................ 4 D.3.1 While .................................................................................................................................. 4 D.3.2 do-while .............................................................................................................................. 4 D.3.3 for ....................................................................................................................................... 5 D.4 Excepciones ............................................................................................................................... 6 8 Apéndice E Ejecuciónde JavasinBlueJ Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 ApéndiceE‐EjecucióndeJavasinBlueJ A lo largo de este libro hemos usado BlueJ para desarrollar y ejecutar nuestras aplicaciones Java. Hay una buena razón para esto: BlueJ nos ofrece algunas herramientas para que resulten más fáciles algunas de las tareas de desarrollo. En particular, nos permite ejecutar fácilmente métodos individuales de clases y de objetos, lo que resulta muy útil si queremos probar rápidamente un fragmento de código. Dividimos la discusión sobre cómo trabajar fuera del entorno BlueJ en dos categorías: ejecutar una aplicación y desarrollarla fuera del entorno BlueJ. E.1 Ejecución sin BlueJ Normalmente, cuando se entregan las aplicaciones a los usuarios finales, son ejecutadas de diferentes maneras. Las aplicaciones tienen un solo punto de comienzo que define el lugar en que empieza la ejecución cuando un usuario inicia la aplicación. El mecanismo exacto que se usa para iniciar una aplicación depende del sistema operativo; generalmente, se hace doble c1ic sobre el icono de la aplicación o se ingresa el nombre de la misma en una línea de comando. El sistema operativo necesita saber qué método o qué clase debe invocar para ejecutar el programa completo. En Java, este problema se resuelve usando un convenio. Cuando se inicia un programa Java, el nombre de la clase se especifica como un parámetro del comando de inicio y el nombre del método es siempre el mismo, el nombre de este método es «main». Por ejemplo, considere el siguiente comando ingresado en una línea de comando, como si fuera un comando de Windows o de una terminal Unix: java Juego El comando java inicia la máquina virtual de Java, que forma parte del kit de desarrollo de Java (JDK) y que debe estar instalado en su sistema. Juego es el nombre de la clase que queremos iniciar. Entonces Java buscará un método en la clase Juego cuya signatura coincida exactamente con la siguiente: public static void main (String [] args) El método debe ser público para que pueda ser invocado desde el exterior de la clase. Debe ser static porque no existe ningún objeto cuando se inicia el programa; inicialmente, tenemos sólo clases, motivo por el cual sólo podemos invocar métodos estáticos. Este método estático crea el primer objeto. El tipo de retorno es void ya que este método no devuelve ningún valor. Aunque el nombre «main» fue seleccionado arbitrariamente por los desarrolladores de Java, es fijo: el método debe tener siempre este nombre. (La elección de «main» como nombre del método inicial en realidad proviene del lenguaje C, del que Java hereda gran parte de su sintaxis.) El parámetro es una matriz de String, que permite a los usuarios pasar argumentos adicionales. En nuestro ejemplo, el valor del parámetro args será un arreglo de longitud cero. Sin embargo, la línea de comandos que inicia el programa puede definir argumentos: java Juego 2 Fred En esta línea de comando, cada palabra ubicada a continuación del nombre de la clase será leída como un String independiente y pasado al método main como un elemento del arreglo de String. En este caso, el arreglo args contendrá dos elementos que son las cadenas «2» y «Fred». Los parámetros en la línea de comandos no son muy usados en Java. 2 En teoría, el cuerpo del método main puede contener el número de sentencias que se deseen. Sin embargo, un buen estilo indica que el método main debiera mantenerse lo más corto posible; específicamente, no debiera contener nada que forme parte de la lógica de la aplicación. En general, el método main debe hacer exactamente lo que se hizo interactivamente para iniciar la misma aplicación en BlueJ. Por ejemplo, si para iniciar la aplicación en BlueJ se creó un objeto de la clase Juego y se invocó el método de nombre start, en el método main de la clase Juego deberían agregarse las siguientes sentencias: public static void main(String[]args) { Juego juego new Juego(); juego.start(); } Ahora, al ejecutar el método main se imitará la invocación interactiva del juego. Los proyectos Java se guardan generalmente en un directorio independiente para cada uno y todas las clases del proyecto se ubican dentro de este directorio. Cuando se ejecute el comando para iniciar Java y ejecutar su aplicación, se debe asegurar de que el directorio del proyecto sea el directorio activo en la terminal de comandos, lo que asegura que se encontrarán las clases que se usan. Si no puede encontrar una clase específica, la máquina virtual de Java generará un mensaje de error similar a este: Exception in thread "main" java.lang.NoClassDefFoundError: Juego Si ve un mensaje como éste, asegúrese de que escribió correctamente el nombre de la clase y de que el directorio actual realmente contenga esta clase. La clase se guarda en un archivo de extensión ". class": por ejemplo, el código de la clase Juego se almacena en un archivo de nombre Juego.class. Si encuentra la clase pero ésta no contiene un método main o el método main no posee la signatura correcta verá un mensaje similar a este: Exception in thread "main " java.lang.NoSuchMethodError: main En este caso, asegúrese de que la clase que quiere ejecutar tenga el método main correcto. E.2 Crear archivos ejecutables .jar Los proyectos Java se almacenan como una colección de archivos en un directorio (o carpeta). A continuación, hablaremos brevemente sobre los diferentes tipos de archivo. Generalmente, para distribuir aplicaciones a otros usuarios es más fácil si toda la aplicación se guarda en un único archivo; el mecanismo de Java que realiza esto tiene el formato de archivo Java (.jar). Todos los archivos de una aplicación se pueden reunir en un único archivo y aun así podrán ser ejecutados. Si está familiarizado con el formato de compresión «zip», sería interesante saber que, de hecho, el formato es el mismo. Los archivos jar pueden abrirse mediante programas zip y viceversa. Para crear un archivo .jar ejecutable es necesario especificar la clase principal en algún lugar. Recuerde: el método que se ejecuta siempre es el main, pero necesitamos especificar la clase que lo contiene. Esta especificación se hace incluyendo un archivo de texto en el archivo .jar el archivo explícito con la información necesaria. Afortunadamente, BlueJ se ocupa por su propia cuenta de esta tarea. 3 Para crear un archivo ejecutable .jar en BlueJ use la función Project - Create Jar File y especifique la clase que contiene el método main en la caja de diálogo que aparece. Debe escribir un método main exactamente igual al descrito anteriormente. Para ver detalles sobre esta función, lea el Tutorial de BlueJ al que puede acceder mediante el menú Help-Tutorial de BlueJ o bien visitando el sitio web de BlueJ. Una vez que se creó el archivo ejecutable .jar, se puede ejecutar haciendo doble c1ic sobre él. La computadora que ejecuta este archivo jar debe tener instalado el JDK (Java Development Kit) o el JRE (Java Runtime Environment) y con él deben estar asociados los archivos .jar. E.3 Desarrollo sin BlueJ Si no quiere solamente ejecutar programas, sino que también quiere desarrollarlos fuera del entorno BlueJ, necesitará editar y compilar las clases. El código de una clase se almacena en un archivo de extensión «. java»; por ejemplo, la clase Juego se almacena en un archivo de nombre Juego.java. Los archivos fuente pueden editarse con cualquier editor de textos. Existen muchos editores de textos libres o muy baratos. Algunos, como el Notepad o el WordPad se distribuyen con Windows, pero si en realidad quiere usar un editor para hacer algo más que una prueba rápida, querrá obtener uno mejor. Sin embargo, sea cuidadoso con los procesadores de texto: generalmente los procesadores de texto no graban en formato de texto plano y Java no podrá leerlos. Los archivos fuente pueden compilarse desde una línea de comando usando el compilador Java que se incluye en el JDK y que se invoca mediante el comando javac. Para compilar un archivo fuente de nombre Juego.java use el comando javac Juego.java. Este comando compilará la clase Juego y cualquier otra clase que dependa de ella. Creará un archivo denominado Juego.class que contiene el código que puede ser ejecutado mediante la máquina virtual de Java. Para ejecutar este archivo use el comando java Juego Observe que este comando no incluye la extensión del archivo .class. 4 Apéndice F Depurador Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 ApéndiceF‐UtilizacióndelDepurador El depurador de BlueJ proporciona un conjunto de funcionalidades básicas de depuración intencionalmente simplificadas y que son genuinamente útiles tanto para la depuración de programas como para alcanzar mayor comprensión del comportamiento de la ejecución de un programa. Se puede acceder a la ventana del depurador seleccionando el elemento Show Debugger del menú View o presionando el botón derecho del ratón sobre el indicador de trabajo y seleccionando Show Debugger desde el menú contextual. La Figura F.l muestra la ventana del depurador. Figura F.l La ventana del depurador tiene cinco zonas de visualización y cinco botones de control. Las zonas de visualización y los botones se activan solamente cuando un programa alcanza un punto de interrupción o se para por alguna otra razón. Las siguientes secciones describen cómo establecer puntos de interrupción para controlar la ejecución de un programa y el propósito de cada una de las zonas. 2 F.1 Puntos de interrupción Un punto de interrupción es una bandera que se asocia con una línea de código (Figura F.2). Cuando se alcanza un punto de interrupción durante la ejecución de un programa, se activan las zonas de visualización y los controles del depurador permitiendo inspeccionar el estado del programa y controlar la ejecución a partir de allí. Figura F.2 Los puntos de interrupción se establecen en la ventana del editor, ya sea presionando el botón izquierdo del ratón en la zona de puntos de interrupción situada a la izquierda del código o bien ubicando el cursor en la línea de código en la que debiera estar el punto de interrupción y seleccionando la opción Set/Clear Breakpoinl del menú Tools del editor. Se pueden eliminar los puntos de interrupción mediante el proceso inverso. Sólo se pueden fijar puntos de interrupción en el código de las clases que hayan sido previamente compiladas. F.2 Los botones de control La Figura F.3 muestra los botones de control que se activan ante un punto de interrupción. Figura F.3 F.2.1 Halt El botón Hall está activo cuando el programa se está ejecutando, para permitir que la ejecución se pueda interrumpir, de ser necesario. Si la ejecución se detiene, el depurador mostrará el estado del programa como si hubiera alcanzado un punto de interrupción F.2.2 Step El botón Step ejecuta la sentencia actual. La ejecución se detendrá nuevamente cuando se complete dicha sentencia. Si la sentencia involucra una llamada a método, se completa la llamada al método antes de que la ejecución se detenga nuevamente (a menos que el método invocado tenga otro punto de interrupción explícito). 3 F.2.3 Step Into El botón Step Into ejecuta la sentencia actual. Si esta sentencia es una llamada a un método entonces la ejecución se introducirá en ese método y se detendrá nuevamente en la primer sentencia del mismo. F.2.4 Continue El botón Continue continúa la ejecución del programa hasta que se alcance el siguiente punto de interrupción, se interrumpa la ejecución mediante el botón Halt o se complete la ejecución normalmente. F.2.5 Terminate El botón Terminate finaliza agresivamente la ejecución del programa actual de manera tal que no puede ser detenida nuevamente. Si se desea simplemente interrumpir la ejecución para examinar el estado actual del programa es preferible utilizar la operación Halt. F.3 Las áreas de visualización de variables La Figura F.4 muestra las tres zonas activas en las que se muestran las variables cuando se encuentra un punto de interrupción, en un ejemplo tomado de la simulación predadorpresa trabajada en el Capítulo 10. Las variables estáticas se muestran en la zona superior, las variables de instancia en la del medio y las variables locales en la zona inferior. Figura F.4 Cuando se alcanza un punto de interrupción, la ejecución se detendrá en una sentencia de un objeto arbitrario dentro del programa actual. La zona de variables estáticas (Static variables) muestra los valores de las variables estáticas definidas en la clase de dicho objeto. La zona de variables de instancia (Instance variables) muestra las variables de instancia de dicho objeto en particular. Ambas zonas también incluyen las variables heredadas de las superclases. La zona de variables locales (Local variables) muestra los valores de las variables locales y de los parámetros del método o del constructor que se está ejecutando actualmente. Las variables locales aparecerán en esta zona sólo una vez que hayan sido inicializadas ya que solamente comienzan a existir en la máquina virtual de Java a partir de ese momento. 4 F.4 El área de Call Sequence La Figura F.5 muestra la zona Call Sequence que contiene una secuencia de cuatro métodos de profundidad. Los métodos aparecen en la secuencia en el formato Class.méthod, independientemente de si son métodos estáticos o métodos de instancia. Los constructores aparecen en la secuencia como Clase.<init>. Figura F.5 La secuencia de llamadas opera como una pila: el método que aparece en la parte superior de la secuencia es donde reside actualmente el flujo de la ejecución. Las zonas que muestran a las variables reflejan los detalles del método o del constructor que esté resaltado en ese momento en la secuencia de llamadas. Al seleccionar una línea diferente de la secuencia de llamadas se actualizarán los contenidos de las otras zonas. F.5 El área de visualización Threads Esta zona está fuera del alcance de este libro y no será tratada. 5 Apéndice F - Utilización del Depurador .................................................................................................. 2 F.1 Puntos de interrupción ............................................................................................................... 3 F.2 Los botones de control ............................................................................................................... 3 F.2.1 Halt ..................................................................................................................................... 3 F.2.2 Step ..................................................................................................................................... 3 F.2.3 Step Into ............................................................................................................................. 4 F.2.4 Continue ............................................................................................................................. 4 F.2.5 Terminate............................................................................................................................ 4 F.3 Las áreas de visualización de variables ..................................................................................... 4 F.4 El área de Call Sequence ........................................................................................................... 5 F.5 El área de visualización Threads ............................................................................................... 5 6 Apéndice I Javadoc Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 ApéndiceI‐Javadoc La escritura de buena documentación de las definiciones de las clases y de las interfaces es un complemento importante para obtener código de buena calidad. La documentación le permite al programador comunicar sus intenciones a los lectores humanos en un lenguaje natural de alto nivel, en lugar de forzarlos a leer código de nivel relativamente bajo. La documentación de los elementos públicos de una clase o de una interfaz tienen un valor especial, pues los programadores pueden usarla sin tener que conocer los detalles de su implementación. En todos los proyectos de ejemplo de este libro hemos usado un estilo particular de comentarios que es reconocido por la herramienta de documentación javadoc que se distribuye como parte del kit de desarrollo (JDK) de Java de Sun Microsystem. Esta herramienta automatiza la generación de documentación de clases en formato HTML con un estilo consistente. El API de Java ha sido documentado usando esta misma herramienta y se aprecia su valor cuando se usa la biblioteca de clases. En este apéndice hacemos un breve resumen de los principales elementos de los comentarios de documentación que deberá introducir habitualmente en su propio código fuente. I.1 Comentarios de documentación Los elementos de una clase que se documentarán son la definición de la clase, sus campos, constructores y métodos. Desde el punto de vista de un usuario, lo más importante de una clase es que tenga documentación sobre ella y sobre sus constructores y métodos públicos. Tendemos a no proporcionar comentarios del estilo de javadoc para los campos aunque recordamos que forman parte del detalle del nivel de implementación y no es algo que verán los usuarios. Los comentarios de documentación comienzan siempre con los tres caracteres "/**" y terminan con el par de caracteres "*/". Entre estos símbolos, un comentario contendrá una descripción principal seguida por una sección de etiqueta, aunque ambas partes son opcionales. I.1.1 La descripción principal La descripción principal de una clase debiera consistir en una descripción del objetivo general de la clase. El Código I.I muestra parte de una típica descripción principal, tomada de la clase Juego del proyecto world-of-zuul. Observe que la descripción incluye detalles sobre cómo usar esta clase para iniciar el juego. /** * Esta es la clase principal de la aplicación "World of Zuul" * "World of Zuul" es un juego de aventuras muy sencillo, basado en texto. * Los usuarios pueden caminar por algún escenario, y eso es todo lo * que hace el juego. ¡Podría ampliarse para que resulte más interesante! * Para jugar, cree una instancia de esta clase e invoque el método "jugar" */ 2 La descripción principal de un método debiera ser bastante general, sin introducir demasiados detalles sobre su implementación. En realidad, la descripción principal de un método generalmente consiste en una sola oración, como por ejemplo / ** * Crea un nuevo pasajero con distintas ubicaciones de * salida y de destino. */ Las ideas esencia les debieran presentarse en la primera sentencia de la descripción principal de una clase, de una interfaz o de un método ya que es lo que se usa a modo de resumen independiente en la parte superior de la documentación generada. Javadoc también soporta el uso de etiquetas HTML en sus comentarios. I.1.2 La sección de marcadores A continuación de la descripción principal aparece la sección de marcadores. Javadoc reconoce alrededor de 20 marcadores distintos pero sólo trataremos las más importantes (Tabla I.l). Los marcadores pueden usarse de dos maneras: Marcadores de bloques. Marcadores incrustados. Sólo hablaremos de los marcadores de bloques pues son los que se usan con mayor frecuencia. Para ver más detalles sobre las etiquetas de una sola línea y sobre las restantes etiquetas, puede recurrir a la sección Javadoc de la documentación Tools and Utilities que forma parte del Java JDK. Etiqueta Texto asociado @author nombre(s) del autor(es) @param nombre de parámetro y descripción @return descripción del valor de retorno @see referencia cruzada @throws tipo de excepción que se lanza y las circunstancias en las que se hace @version descripción de la versión Los marcadores @author y @version se encuentran regularmente en los comentarios de una clase y de una interfaz y no pueden usarse en los comentarios de métodos, constructores o campos. Ambos marcadores pueden estar seguidos de cualquier texto y no se requiere ningún formato especial para ninguna de ellos. Ejemplos: @author Hakcer T. LargeBrain @version 2004.12.31 3 Los marcadores @param y @throws se usan en métodos y en constructores, mientras que @return se usa sólo en métodos. Algunos ejemplos: @param limite El valor máximo permitido. @return Un número aleatorio en el rango 1 a limite (inclusive). @throws IllegalLimitException Si el límite es menor que 1. La etiqueta @see adopta varias formas diferentes y puede usarse en cualquier comentario de documentación. Proporciona un camino de referencia cruzada hacia un comentario de otra clase, método o cualquier otra forma de documentación. Se agrega una sección See Also al elemento que está siendo comentado. Algunos ejemplos típicos: @see "The Java Language Specification, by Joy et al" @see <a href=http://www.bluej .org/>The BlueJ web site </a> @see #estaVivo @see java.util.ArrayList#add El primer marcador simplemente encierra un texto en forma de cadena sin un hipervínculo. El segundo es un hipervínculo hacia el documento especificado. El tercero es un vínculo a la documentación del método estaVivo de la misma clase. El cuarto vincula la documentación del método add de la clase ArrayList del paquete java.util. I.2 Soporte de BlueJ para javadoc Si un proyecto ha sido comentado usando el estilo de Javadoc, BlueJ ofrece utilidades para generar la documentación HTML completa. En la ventana principal, seleccione el elemento Tools/Project Documentation del menú y se generará la documentación (si es necesario) y se mostrará en la ventana de un navegador. Dentro del editor de BlueJ, se puede pasar de la vista del código fuente de una clase a la vista de su documentación cambiando la opción Source Code a Documentation en la parte derecha de la ventana del menú Tools del editor. Esta opción ofrece una vista previa y rápida de la documentación pero no contendrá referencias a la documentación de las superclases o de las clases que se usan. 4 Apéndice I - Javadoc ................................................................................................................................ 2 I.1 Comentarios de documentación ................................................................................................. 2 I.1.1 La descripción principal ..................................................................................................... 2 I.1.2 La sección de marcadores................................................................................................... 3 I.2 Soporte de BlueJ para javadoc ................................................................................................... 4 5 Apéndice J Estilo de Programación Centro Asociado Palma de Mallorca Tutor: Antonio Rivero Cuesta 1 Apéndice J - Estilo de Programación J.1 Nombres J.1.1 Use nombres significativos Use nombres descriptivos para todos los identificadores (nombres de clases, de variables, de métodos). Evite ambigüedades. Evite abreviaturas. Están formados por letras y dígitos. No pueden empezar por un dígito. No pueden contener ninguno de los caracteres especiales vistos en una entrada anterior. Los caracteres especiales y signos de puntuación siguientes: +-*/=%&#!?^“‘~\|<>()[]{}:;., No puede ser una palabra reservada de Java. Las palabras reservadas en Java. Los métodos de modificación deben comenzar con el prefijo "set": setAlgo( .. ). Los métodos de acceso deben comenzar con el prefijo "get": getAlgo( .. ). Los métodos de acceso con valores de retorno booleanos generalmente comienzan con el prefijo "es": esAlgo(..); por ejemplo, esVacio( ). J.1.2 Los nombres de las clases comienzan con una letra mayúscula. J.1.3 Los nombres de las clases son sustantivos en singular. J.1.4 Los nombres de los métodos y de las variables comienzan con letras minúsculas. Tanto los nombres de las clases, como los de los métodos y los de las variables, emplean letras mayúsculas entre medio para aumentar la legibilidad de los identificadores que lo componen; por ejemplo: numeroDeElementos. J.1.5 Las constantes se escriben en MAYÚSCULAS Ocasionalmente se utiliza el símbolo de subrayado en el nombre de una constante para diferenciar los identificadores que lo componen: TAMANIO_MAXIMO. J.2 Esquema J.2.1 Un nivel de indentación es de cuatro espacios J.2.2 Todas las sentencias de un bloque se indentan un nivel 2 J.2.3 Las llaves de las clases y de los métodos se ubican solas en una línea Las llaves que encierran el bloque de código de la clase y las de los bloques de código de los métodos se escriben en una sola línea y con el mismo nivel de indentación. Por ejemplo: public int getEdad(){ sentencias } J.2.4 Para los restantes bloques de código, las llaves se abren al final de una línea En todos los bloques de código restantes, la llave se abre al final de la línea que contiene la palabra clave que define al bloque. La llave se cierra en una línea independiente, alineada con la palabra clave que define dicho bloque. Por ejemplo: while(condición){ sentencias } if(condición){ sentencias } else{ sentencias } J.2.5 Use siempre llaves en las estructuras de control. Se usan llaves en las sentencias if y en los ciclos aun cuando el cuerpo esté compuesto por una única sentencia. J.2.6 Use un espacio antes de la llave de apertura de un bloque de una estructura de control. J.2.7 Use un espacio antes y después de un operador. J.2.8 Use una línea en blanco entre los métodos (y los constructores). Use líneas en blanco para separar bloques lógicos de código; es decir, use líneas en blanco por lo menos entre métodos, pero también entre las partes lógicas dentro de un mismo método. J.3 Documentación J.3.1 Cada clase tiene un comentario de clase en su parte superior El comentario de clase contiene como mínimo • Una descripción general de la clase • El nombre del autor (o autores) • Un número de versión 3 Cada persona que ha contribuido en la clase debe ser nombrada como un autor o debe ser acreditada apropiadamente de otra manera. Un número de versión puede ser simplemente un número o algún otro formato. Lo más importante es que el lector pueda reconocer si dos versiones no son iguales y determinar cuál es la más reciente. J.3.2 Cada método tiene un comentario J.3.3 Los comentarios son legibles para javadoc Los comentarios de la clase y de los métodos deben ser reconocidos por javadoc; en otras palabras: deben comenzar con el símbolo de comentario «/**». J.3.4 Comente el código sólo donde sea necesario Se deben incluir comentarios en el código en los lugares en que no resulte obvio o sea difícil de comprender (y preferentemente, el código debe ser obvio o fácil de entender, siempre que sea posible) y donde ayude a la comprensión de un método. No comente sentencias obvias, ¡asuma que el lector comprende Java! J.4 Restricciones de uso del lenguaje J.4.1 Orden de las declaraciones: campos, constructores, métodos. Los elementos de una definición de clase aparecen (si se presentan) en el siguiente orden: sentencias de paquete, sentencias de importación, comentario de clase, encabezado de la clase, definición de campos, constructores, métodos. J.4.2 Los campos no deben ser públicos (con excepción de los campos final). J.4.3 Use siempre modificadores de acceso. Especifique todos los campos y los métodos como privados, públicos o protegidos. Nunca use el acceso por defecto (package private). J.4.4 Importe las clases individualmente Es preferible que las sentencias de importación nombren explícitamente cada clase que se quiere importar y no al paquete completo. Por ejemplo: import java.util.ArrayList; import java.util.HashSet; Es mejor que: import java.util.*; J.4.5 Incluya siempre un constructor (aun cuando su cuerpo quede vacío). J.4.6 Incluya siempre una l/amada al constructor de una superclase. En los constructores de las subclases no deje que se realice la inserción automática de una llamada a una superclase; incluya explícitamente la invocación super( ... ), aun cuando funcione bien sin hacerlo. 4 J.4.7 Inicialice todos los campos en el constructor. J.5 Modismos del código J.5.1 Use iteradores en las colecciones Para iterar o recorrer una colección, use un ciclo for-each. Cuando la colección debe ser modificada durante una iteración, use un Iterator en lugar de un índice entero. 5