Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Java XD

Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1de 213

Aprender

la Programación Orientada a Objetos


con el lenguaje Java
Este libro se dirige a los estudiantes y desarrolladores que hayan tenido previamente una primera
experiencia con la programación estructurada y que deseen pasar a la Programación Orientada a Objetos
(POO) con el lenguaje Java, para desarrollar aplicaciones portables.

Después de una breve historia de la POO y del lenguaje Java, el autor explica por qué este tipo de
programación se ha convertido en algo imprescindible para desarrollar en los entornos gráficos orientados a
eventos. Se presentan las nociones de objeto, clase y referencia, para pasar a los fundamentos de la POO,
que son la encapsulación, la herencia, el polimorfismo y la abstracción. También se exponen las
diferentes etapas de un desarrollo orientado a objetos, con los principios de la modelización UML.

Posteriormente, el autor presenta la máquina virtual Java, su interés, riqueza y un entorno de desarrollo con
IntelliJ IDEA de la empresa JetBrains. El lector descubrirá cómo Java reproduce los principios de la POO,
siguiendo unas sencillas explicaciones, con unos ejemplos concretos y realizando ejercicios de entrenamiento.
También descubrirá los tipos básicos del desarrollo Java y su utilización, cómo explotar un IDE para
simplificar la escritura de los programas y ponerlos a punto. Los programas de prueba son de tipo consola o
gráficos, basados en la utilización de Swing para ilustrar las comunicaciones entre objetos. Cuando es
pertinente, se exponen en paralelo códigos con los lenguajes de programación orientados a objetos C++ y C#.
Se presenta la programación multithread, que permite la ejecución simultánea de varios flujos de
instrucciones, seguido de una introducción a las pruebas unitarias tan importantes para asegurar la fiabilidad
de los objetos. Para terminar, el último capítulo se dedica a la reflexión en Java, que promete algunas
sorpresas.

Al terminar este libro, el lector dispondrá de unas sólidas bases para aprender los potentes API Java y realizar
programas orientados a objetos modulares, fiables y extensibles.

Hay disponibles elementos adicionales, que se pueden descargar en esta página.

Luc GERVAIS
Desde hace veinticinco años, Luc GERVAIS ejerce la profesión de desarrollador de software. Provenía del
mundo de la electrónica y empezó a programar en lenguaje ensamblador para pasar posteriormente a C, C++,
C# y Java. Ha impartido muchas sesiones de formación sobre estos diferentes lenguajes de programación,
tanto para un público formado por desarrolladores profesionales, como por estudiantes (Universidad de
Rouen). Esta doble experiencia (técnica y pedagógica), le permite responder perfectamente a las expectativas
de los lectores que deseen pasar a la Programación Orientada a Objetos (POO) con el lenguaje Java.
Introducción
La programación orientada a objetos (POO) se ha convertido en algo inevitable para la gran mayoría de los
desarrolladores. Este libro le va a presentar los grandes principios, destacados gracias a ejemplos de código
sencillo, escritos en lenguaje Java. Por lo tanto, en este libro se le presentan dos aprendizajes de manera
paralela.

Entender los fundamentos de la POO es capital para, con posterioridad, poder beneficiarse completamente de la
potencia de los lenguajes como C++, C#, PHP y, por supuesto, Java. En determinadas ocasiones, el aprendizaje
de la POO puede parecer difícil, incluso desagradable. Al inicio, el desarrollador se deprime; lucha por entender
esta nueva organización y el interés que podría tener respecto a la programación procedural «clásica». Pero de
manera muy rápida, se produce el «clic» que invierte la tendencia, cayendo de repente en una euforia creativa.
Rápidamente, el desarrollador desea refactorizar sus aplicaciones existentes, explotando estos nuevos
conceptos. Luego, arranca nuevos desarrollos «completamente orientados a objetos», construye una batería de
herramientas verificadas que puede hacer evolucionar a su voluntad, etc. Finalmente, vuelve a una
programación procedural, incluso a lo largo de un mismo proyecto, haciendo que todo se convierta sencillamente
en una tortura.

Piense de modo orientado a objetos y, a continuación, programe objetos que le permiten diseñar aplicaciones
modulares, con gran rendimiento y fiables. Descubrirá que finalmente es más sencillo de lo que parece porque
está muy próximo a nuestra realidad.

He aquí lo que encontrará en este libro.

Después de un histórico de la POO y del lenguaje Java, verá los aspectos en los que este tipo de
programación se ha hecho imprescindible para desarrollar en entornos gráficos orientados a eventos. Se
presentarán las nociones de objeto, clase y referencia, seguidas de los fundamentos de la POO, que son la
encapsulación, la herencia, el polimorfismo y la abstracción. Se abordarán las diferentes etapas de un
desarrollo orientado a objetos con los principios de modelización UML, seguidas por una presentación de la
plataforma Java, su interés, riqueza y el entorno de desarrollo IntelliJ IDEA de la empresa JetBrains, en Microsoft
Windows. Verá cómo Java reproduce los principios de la POO con explicaciones sencillas, ejemplos concretos
y ejercicios corregidos, que se pueden descargar desde la página Información. Se presentan los tipos básicos
del desarrollo Java y sus formas de uso. Los programas de prueba son de tipo consola o gráfico, basados en la
utilización de la librería Swing, para ilustrar las comunicaciones entre objetos. Cuando sea pertinente, los
ejemplos se explicarán con los lenguajes de POO C++ y C#. Se presentará el aspecto
programación multithread, que permite la ejecución simultánea de varios flujos de instrucciones, seguido de
una introducción a las pruebas unitarias, fundamentales para dar fiabilidad a los objetos. Para terminar, el último
capítulo se dedica a la reflexión en Java, que nos promete algunas sorpresas.

Sea bienvenido al mundo de la POO, al mundo del lenguaje Java, que es el lenguaje más adaptado a la
programación multi-entorno.

Los ejercicios de este libro se pueden descargar desde la página Información.


1. INTRODUCCIÓN A LA POO
Historia de la POO
Lo que sigue a continuación no pretende ser exhaustivo. Sencillamente repasa los eventos principales que
permitieron la democratización de la POO y la creación del lenguaje Java.

Al contrario de lo que se podrá pensar, el concepto de la POO no es reciente. En los años 60, dos brillantes
investigadores noruegos, Kristen Nygaard y Ole-Johan Dahl, desarrollaron las bases de la POO creando el
lenguaje Simula. Las nociones básicas de la POO como las clases, la herencia, los métodos virtuales, etc., se
crearon en este lenguaje, para permitir modelizar de manera fiel procesos industriales complejos. Simula-67
abrió el camino a los lenguajes orientados a objetos como Smalltalk y a continuación, entre otros, a C++, Java y
C#, que empezaron a explotar estos conceptos algunos lustros más tarde. Los dos autores de Simula fueron
recompensados por sus trabajos al inicio de los años 2000, justo antes de su desaparición...

En 1980, Smalltalk es el primer lenguaje objeto propuesto con un entorno de desarrollo gráfico integrado.
Smalltalk se diseñó por el equipo del americano Alan Kay en el centro de investigación informática de Xerox en
California (el famoso Palo Alto Research Center). Este lenguaje retoma y completa los conceptos básicos,
principalmente la noción de compilación dinámica de un código «intermedio», portable a entornos heterogéneos
en un código máquina destino. Este concepto se retomará por Java con su Just In Time Compiler y por C#.

Todavía en 1980, el danés Bjarne Stroustrup desarrolla para AT&T el lenguaje C++ (inicialmente llamado C with
classes). C++ es una evolución orientada a objetos del lenguaje C, inventado por el célebre tándem canado-
estadounidense Kernighan y Ritchie para UNIX, 10 años antes. El lenguaje C++ es muy utilizado incluso por sus
detractores, que lo consideran demasiado complejo y no tan orientado a objetos, debido a su compatibilidad con
C.

En los años de 1990, James Gosling desarrolla el lenguaje Oak para Sun Microsystems. Oak será rebautizado
como Java en 1995. Este lenguaje se pensó para ser independiente del hardware que ejecuta sus programas
«precompilados», para ser robusto y seguro y, sobre todo, más fácil de programar que el C++. En la actualidad,
el lenguaje Java se utiliza extensamente tanto en entornos web como en puestos de sobremesa, teléfonos y
otras tabletas. Pertenece a Oracle, que adquirió Sun Microsystems en abril de 2009 por más de 7000 millones de
dólares.

En 2001, Microsoft presentó C# (pronunciado C Sharp, siendo sharp la traducción inglesa del sostenido musical y
C por el do), un lenguaje desarrollado por el danés Anders Hejlsberg, que es el creador del Turbo Pascal y el
arquitecto de Delphi. C# es un lenguaje objeto cercano a Java, que permite programar aplicaciones
principalmente en entornos Microsoft, tales como los puestos de escritorio, las aplicaciones web, los
smartphones o las tabletas.

Incluso si el lenguaje C# tiene posibilidades sintácticas más avanzadas que Java, su utilización se dirige
principalmente a la programación de aplicaciones .NET para entornos Microsoft, mientras que Java es
independiente de los sistemas operativos. Las aplicaciones escritas en Java pueden funcionar en Windows, Linux,
Mac OS o UNIX.

¿Por qué utilizar la POO?

Los años 70-80 vieron nacer los lenguajes procedurales con una ejecución lineal de sus programas, yendo desde
el inicio hasta el final.

Evidentemente reservado para desarrollos pequeños, este tipo de programación evolucionó rápidamente hacia
una programación llamada estructurada, donde procedimientos y funciones descomponían el programa en
operaciones unitarias.
Un poco a imagen del funcionamiento interno de los ordenadores, la programación estructurada no mezcla
fácilmente variables y operaciones. Por lo general, en la parte superior de código hay una lista consecuente de
definiciones de variables, que se utilizarán seguidamente en las diferentes funciones.

Las aplicaciones se hacen cada vez más complejas y los entornos de ejecución evolucionan hacia las interfaces
gráficas, por lo que la programación estructurada rápidamente se vuelve difícil de escribir, de mantener y de
hacer evolucionar.

Inspirándose en los conceptos propuestos por Simula, se llega a los lenguajes orientados a objetos.

La POO aporta a los desarrolladores medios para afrontar los nuevos retos, que son los suyos, con:

una organización modular muy próxima a la realidad,

una creación, puesta a punto y mantenimiento de los componentes más fácil y rápida,

la reutilización y evolución de los componentes existentes o de terceros,

una integración fácil para un funcionamiento en entornos gráficos,

una lógica de codificación compatible con las aplicaciones distribuidas, que distribuyen sus contenidos
entre varias máquinas,

un desacoplamiento de la aplicación, que permite un trabajo en equipo más eficaz y productivo.


Histórico del lenguaje Java
Desde las primeras versiones de Windows, el lenguaje C y posteriormente C++ se utilizaron mucho para
construir aplicaciones. A pesar de su orientación a objetos y su potencia incontestable, C++ se muestra como un
lenguaje complejo de utilizar. El desarrollador debe administrar absolutamente todo, como por ejemplo las
asignaciones/liberaciones de memoria y los aspectos relacionados con la gestión de la seguridad. Además, la
utilización directa de las aplicaciones en interfaces Windows impacta de manera inmediata en la estabilidad del
sistema en caso de funcionamiento incorrecto y errores.

En los años 90, el lenguaje Java ofrecía a los desarrolladores una codificación de sus programas mucho más
sencilla y una ejecución aislada en una máquina virtual. Es suficiente con que un entorno de explotación tenga
una máquina virtual Java para que la aplicación funcione sin modificar el código. Además, la máquina virtual
garantiza una utilización correcta de los recursos reales, particionando la ejecución de las aplicaciones. Respecto
a la gestión de memoria, el desarrollador administra únicamente las peticiones de asignación, a medida que
evolucionan las necesidades de su programa. Las zonas de memoria se explotan hasta que terminan las
operaciones y el dispositivo interno, conocido con el nombre de garbage collector, es el encargado de detectar
las zonas de memoria que ya no se utilizan y administra su liberación.

Durante la explosión de Internet, Java se impulsa en las empresas con el navegador diseñado por Netscape, que
integraba esta tecnología. Frente a este éxito, Sun Microsystems decide publicar una plataforma de desarrollo
gratuita a finales del año 1995. Esta plataforma, que en la actualidad es propiedad de Oracle, encargado
también de su mantenimiento, no para de evolucionar. La versión 9 está disponible desde septiembre del año
2017.
2. EL DISEÑO ORIENTADO A OBJETOS
Enfoque procedural y de descomposición funcional
Antes de enumerar los conceptos básicos de la POO, vamos a revisar el enfoque procedural gracias a un ejemplo
concreto de organización de código.

En este libro encontraremos habitualmente los términos «código» y «codificación»; no se trata ni de alarmas ni
de funciones de cifrado de contraseña. El código o código fuente es el nombre que se da al contenido que el
desarrollador entra durante todo el día en su editor de texto favorito, para posteriormente convertirse (o incluso
compilarse) en un flujo de instrucciones ejecutables por el ordenador.

La programación procedural es un paradigma de programación, que considera los diferentes actores de un


sistema como objetos prácticamente pasivos que un procedimiento central utilizará para una función
determinada.

Tomemos como ejemplo la distribución de agua corriente en nuestras casas e intentemos modelizar este
principio en una aplicación muy sencilla. El análisis procedural (igual que el análisis orientado a objetos) muestra
una lista de objetos como los siguientes:

el grifo del fregadero;

el depósito de agua;

un detector del nivel de agua con un contador en el depósito;

la bomba de alimentación que lleva el agua al río.

El código del programa «procedural» consistiría en crear un conjunto de variables que represente a los
argumentos de cada componente y, a continuación, escribir un bucle de operación de la gestión central,
probando los valores leídos y actuando en función del resultado de las pruebas. Observe que por un lado hay
variables y por otro, acciones.
La transición hacia el enfoque orientado a objetos
La POO es un paradigma de programación que considera los diferentes actores de un sistema como objetos
activos y relacionados. El enfoque orientado a objetos normalmente se parece mucho a la realidad.

En nuestro ejemplo:

El usuario abre el grifo.

El grifo libera la presión y el agua fluye desde el depósito hasta el fregadero.

Como nuestro usuario no es el único en consumir, el detector/flotador del depósito llega a un nivel que
pone en marcha la bomba de alimentación del río.

El usuario cierra el grifo.

Alimentado por la bomba, el depósito de agua continúa llenándose hasta que el detector/flotador llegue al
nivel suficiente que detendrá la bomba.

Parada de la bomba.

En este enfoque, puede comprobar que los objetos «interactúan»; no existe operación central que defina
dinámicamente el funcionamiento de los objetos. Ha habido un análisis funcional que ha conducido a la creación
de los diferentes objetos, su realización y su puesta en relación.

El código del programa «objeto» va a seguir esta realidad, proponiendo tantos objetos como se han descrito con
anterioridad, pero definiendo entre estos objetos los métodos de intercambio adecuados, que conducirán al
funcionamiento esperado.

Los conceptos objetos son muy próximos a la realidad.


Las características de la POO

1. El objeto, la clase y la referencia

a. El objeto

El objeto es el elemento básico de la POO. El objeto es la unión de:

una lista de variables de estados;

una lista de comportamientos;

una identificación.

Las variables de estado cambian a lo largo del ciclo de vida del objeto. Tomemos el caso de un reproductor de
música digital. Cuando se compra el aparato, los estados de este objeto podrán ser:

memoria libre = 100 %;

tasa de carga de la batería = baja;

aspecto exterior = nuevo.

A medida que se va utilizando, sus estados se modifican. Rápidamente la memoria libre caerá, la tasa de
carga de la batería variará y el aspecto exterior va a cambiar en función del cuidado que ponga el usuario
durante su utilización.

Los comportamientos del objeto definen lo que se puede hacer con él:

reproducir la música;

ir a la pista siguiente;

ir a la pista anterior;

subir el sonido;

etc.

Una parte de los comportamientos del objeto es accesible desde el exterior de este objeto: Botón Play, Botón
Stop... y otra parte solo es accesible internamente: lectura de la tarjeta de memoria, decodificación de la
música a partir del archivo. Se habla de «encapsulación» para definir el límite entre los comportamientos
accesibles desde el exterior y los comportamientos internos.

La identificación de un objeto es una información, separada de la lista de estados, que permite diferenciar
este objeto particular del resto (es decir, de otros objetos que son del mismo tipo). La identificación puede
ser un número de referencia o una cadena de caracteres única, construida durante la creación del objeto;
también puede ser la dirección de memoria donde se almacena el objeto. En realidad, su forma depende
totalmente de la problemática asociada.

El objeto programado se puede adjuntar a una entidad real, como para nuestro reproductor digital, pero
también se puede adjuntar a una entidad totalmente virtual, como una cuenta de cliente, la entrada de un
directorio telefónico, etc. La finalidad del objeto informático es administrar la entidad física o emularla.

Incluso si este no forma parte de su naturaleza básica, el objeto se puede hacer persistente, es decir, que sus
estados se pueden guardar en un soporte de memoria de información, de manera permanente. A partir de
este almacenamiento, el objeto se podrá reconstruir con sus estados cuando el sistema desee. El
soporte típico capaz de tal hazaña es la base de datos.

b. La clase

La clase es el «molde» a partir del que el objeto se va a crear en memoria. La clase describe los estados y los
comportamientos comunes de un mismo tipo. Los valores de estos estados estarán contenidos en los objetos
de la clase.

Todas las cuentas corrientes de un mismo banco contienen los mismos argumentos (detalles del titular, saldo,
etc.) y todas tienen las mismas funciones (disponer de una cantidad a crédito, a débito, recordar la operación
bancaria si es necesario, etc.). Estas definiciones deben estar contenidas en una clase y, cada vez que un
cliente abre una nueva cuenta, esta clase servirá de modelo durante la creación del nuevo objeto cuenta.

La pantalla de reproductores de música digital ofrece un mismo modelo en diferentes colores, con tamaños
de memoria diferentes y modulables, etc. Cada dispositivo de la pantalla es un objeto que ha sido fabricado a
partir de la información de una única clase. Durante la construcción, los atributos del aparato se seleccionan
en función de criterios estéticos y comerciales decididos por los responsables de producto, teniendo en cuenta
las tendencias de compra de los clientes.

Una clase puede contener muchos atributos. Pueden ser de tipo primitivo -enteros, caracteres, etc.-, así como
de tipos más complejos. En efecto, una clase puede contener una o varias clases de otros tipos. Hablamos
entonces de composición, incluso de «acoplamiento fuerte». En este caso, cuando se elimina la clase
principal, ello implica evidentemente la destrucción de las clases que contiene. Por ejemplo, si una clase
hostal contiene una lista de habitaciones, la eliminación del hostal implica la destrucción de sus habitaciones.

Una clase puede hacer «referencia» a otra clase; en este caso, el acoplamiento se llama «débil» y los objetos
pueden vivir independientemente. Por ejemplo, su PC está relacionado con su impresora. Si su PC pasa a
mejor vida, su impresora funcionará con su nuevo PC. Se habla en este caso de asociación.

Además de sus atributos, la clase contiene también una serie de «comportamientos», es decir, una serie de
métodos con sus firmas y sus implementaciones adjuntas. Estos métodos pertenecen a los objetos y se
utilizan tal cual.

Declaramos la clase y su contenido en un mismo archivo fuente, gracias a una sintaxis que estudiaremos en
detalle. Los desarrolladores C++ apreciarán el hecho de que ya no exista, por un lado, una parte de
definiciones del contenido de la clase y, por otro, una parte de implementaciones. En efecto, en Java (como
sucede en C#), el archivo de programa (de extensión .java) contiene las definiciones de todos los estados,
eventualmente con un valor «de inicio» y las definiciones e implementaciones de todos los comportamientos.
Al contrario de lo sucede con C#, una clase Java se debe definir en un único archivo fuente.

c. La referencia

Los objetos se construyen a partir de la clase, por medio de un proceso llamado instanciación y, por lo tanto,
cualquier objeto es una instancia de una clase. Cada instancia empieza en una ubicación de memoria única.
La dirección de esta ubicación de memoria, conocida con el nombre de puntero por los desarrolladores C y
C++, se convierte en una referencia para los desarrolladores Java y C#.

Cuando el desarrollador necesita un objeto durante una operación, debe realizar dos acciones:

declarar y asignar un nombre a una variable del tipo de la clase que se ha de utilizar;

instanciar el objeto y guardar su referencia en esta variable.

Una vez que se realiza esta instanciación, el programa accederá a las propiedades y métodos del objeto,
utilizando la variable que contiene su referencia. Cada instancia es única. Por el contrario, varias variables
pueden «apuntar» a una misma instancia. Cuando ninguna variable apunta a una instancia dada, el garbage
collector guarda esta instancia como instancia para eliminar.

2. La encapsulación
La encapsulación consiste en crear una especie de caja negra, que contiene de manera interna un mecanismo
protegido y, de manera externa, un conjunto de comandos que van a permitir manipularla. Este juego de
comandos se hace de tal manera que será imposible alterar el mecanismo protegido en caso de una utilización
incorrecta. La caja negra será tan opaca que resultará imposible para el usuario intervenir directamente sobre
el mecanismo.

Como comprenderá, la caja negra no es otra cosa que un objeto con métodos públicos de «alto nivel», que
controlan con rigor los argumentos que se pasan antes de utilizarlos en una operación. Respetando el principio
de la encapsulación, se asegurará de que el usuario del objeto nunca pueda acceder «de manera directa» a sus
datos. Gracias a este control, su objeto se utilizará correctamente y por lo tanto de manera más fiable, y su
puesta a punto será más fácil. En el mundo Java, los métodos que permiten acceder en modo lectura a los
datos son los descriptores de acceso y aquellos que permiten acceder en modo escritura, los mutadores.

Java no tiene equivalencia para las propiedades (properties) del lenguaje C#, que permiten conservar el
aspecto práctico del acceso directo a los datos del objeto, respetando los principios de la encapsulación.

3. La herencia
Otra característica importante de la POO consiste en poder crear una clase partiendo de otra. Esto se llama
herencia. Para explicarla, imaginemos que debemos construir un sistema de gestión de los diferentes tipos de
empleados de una empresa. Un análisis rápido revela una lista de propiedades comunes a todos los puestos.
En efecto, el empleado, ya sea directivo, mando intermedio, director o trabajador normal, siempre tiene un
nombre, un apellido y un número de seguridad social. A esto se le llama generalización; consiste en factorizar
los elementos comunes de un conjunto de clases en una clase más general llamada superclase en Java y
quizás «clase de base» en C# y C++. La clase que hereda de la superclase se llama subclase, clase heredada
incluso clase especializada.

Por lo tanto, la herencia va a evitar la redundancia de información entre los diferentes tipos, creando lo que se
llama una jerarquía de clases. Vamos a crear una clase de base, llamada Empleado, que contiene la
información común para todos los tipos de empleado. Seguidamente, crearemos una clase llamada Ejecutivo,
que heredará de la clase de base Empleado. Ejecutivo heredará los miembros de Empleado (o de manera más
concreta, los miembros que Empleado le habrá legado) y añadirá a esta lista de miembros los aspectos
específicos del tipo Ejecutivo. Diremos que Ejecutivo extiende Empleado (en el mundo Java) o que Ejecutivo
hereda de Empleado (en los mundos C++ y C#).

Podemos continuar la jerarquía diseñando la clase Ingeniero, que hereda de Ejecutivo, que a su vez hereda de
Empleado. Desde un objeto Ingeniero, tendremos acceso a los datos de sus dos clases de base y de esta
manera se va hacia lo más específico.

En C++ es posible heredar de varias clases. Esto puede provocar problemas complicados cuando una clase
hereda de varias clases que, a su vez, heredan de una misma clase de base. Para evitar esto, Java, al igual que
C#, limita la herencia por nivel a una única clase. Evidentemente, puede haber varias herencias en cascada.

Abordaremos más adelante las interfaces, que son tipos de clases «sin código» o con código por defecto desde
Java 9, que especifican los comportamientos en los objetos, y veremos que una clase Java, al igual que una
clase C#, podrá heredar de tantas interfaces como se quiera. Observe que las interfaces similares no existen
en C++.

La herencia no se limita a la reutilización de atributos. También afecta a los comportamientos. Un heredado


naturalmente puede añadir nuevos comportamientos, relacionados con sus aspectos específicos. Pero también
puede sustituir los comportamientos de la clase de base por los suyos. Imaginemos una clase de base que
representa a un Animal, con un comportamiento «Gritar». Este comportamiento no tiene ningún sentido si el
animal no se especifica, por lo tanto, no devuelve nada. La clase Perro hereda de la clase Animal y sustituye su
comportamiento «Gritar» por un comportamiento adaptado a su tipo en retorno: «guau guau». La clase Gato
no cumple este mismo principio y sustituye también el comportamiento «Gritar» del Animal devolviendo un
simpático «miau». Si la aplicación administra una colección de referencias de tipo Animal, que contiene una
instancia de la clase Perro y una instancia de la clase Gato, la llamada al método Gritar de sus dos entradas
provocarán un «guau guau» y a continuación a un «miau».

C# se distingue de Java porque ofrece más opciones para controlar la sustitución.

4. El polimorfismo
Otra punta de lanza de la POO: el polimorfismo. El polimorfismo de los objetos está muy relacionado con la
herencia. La raíz etimológica de la palabra lleva a pensar naturalmente que el objeto puede tomar varias
formas. Para entender en qué medida esto es posible, volvamos a nuestro objeto Ingeniero. Hereda de la clase
Ejecutivo y, por lo tanto, un objeto Ingeniero es una especie de Ejecutivo. Gracias al polimorfismo, allá donde
se espere un objeto Ejecutivo, se podrá utilizar un objeto Ingeniero. Vayamos más allá y recordemos que el
objeto Ingeniero hereda de Ejecutivo, que a su vez hereda de Empleado. Por lo tanto, también se puede decir
que un Ingeniero es una especie de Empleado, lo que significa que allá donde se espere un objeto Empleado,
como habrá entendido, se podrá utilizar un objeto Ingeniero. Consecuencia práctica: por ejemplo, se puede
construir una tabla de objetos de tipo Empleado que contenga tantas entradas como miembros del personal
exista en la empresa. A continuación, se puede instanciar en cada celda de esta tabla los objetos de tipo
Ingeniero, Empleado normal, Comercial, etc., previstos, que hereden de Empleado. No habrá errores de
compilación porque todos los objetos de la tabla serán «tipos de Empleado». Las llamadas a las operaciones se
podrán realizar sobre esta colección -como la edición de los recibos de nómina, los cálculos de pensiones, etc.-;
se hacen totalmente genéricos porque serán métodos publicados por la superclase Empleado. Pero estas
operaciones harán referencia «sobre la marcha» a las subclases de Empleado que permiten, por ejemplo, un
cálculo diferente de las pensiones para los ejecutivos y no ejecutivos.

En función del objeto que se haya instanciado, la operación de base se sustituye por una operación
específica.

Veremos cómo Java ofrece sistemas sencillos y prácticos que permiten saber con seguridad si un objeto de un
determinado tipo también forma parte de otra familia (operador instanceof) y se puede considerar como tal en
una operación gracias a un «cast». También veremos cómo la herencia de interfaces y el polimorfismo se
pueden relacionar estrechamente para hacer que se entiendan objetos diseñados con varios años de diferencia.

El polimorfismo también puede afectar a los comportamientos de un objeto. En efecto, un comportamiento con
el mismo nombre puede aparecer varias veces en un objeto, con la condición de que las firmas (los
argumentos esperados) sean diferentes y permitan hacer la diferencia. Es el compilador el encargado de
deducir, de manera estática en función del código del programa que llama, el comportamiento que se debe
llamar. Imaginemos una clase matemática que ofrece un método CalculaSuperficie; este método podrá
aparecer tantas veces como tipos de superficies posibles existan (CalculaSuperficie de un rectángulo, círculo,
triángulo, etc.). En función del número y del tipo de argumento o de los argumentos que se pasan, el
compilador elegirá el método CalculaSuperficie adecuado.

Observe que, en caso de un polimorfismo de herencia, la elección de la función que se ha de ejecutar es


dinámica.

5. La abstracción
La buena práctica en POO consiste en relacionar sus objetos de la manera más abstracta posible para poder
reutilizarlos en diferentes contextos sin tener que «cambiar todo». Por ejemplo, una fuente de datos puede
provenir de una entrada de teclado, del contenido de un archivo, del flujo recibido de una conexión de red, etc.
Si determinados métodos de su clase esperan como argumento una sucesión de datos, será interesante hacer
abstracción del origen de la fuente, considerándola como un flujo «genérico» (stream).

Otro ejemplo: Java ofrece diferentes clases para administrar las colecciones de datos. La elección de la clase
más adaptada se hará siguiendo diferentes criterios, tales como rapidez de acceso a una ubicación de la
colección o la rapidez de inserción de elementos en cualquier lugar de la colección. Estas optimizaciones
muestran el código de implementación de cada clase «colección» y ofrecen un sistema operativo genérico,
común a todos los tipos que hagan abstracción de estas capas inferiores. El código del programa permanece
prácticamente idéntico sea cual sea el tipo de colección utilizada, para responder de la mejor manera posible a
su necesidad.

La programación orientada a objetos y su aplicación con el lenguaje Java ofrecen diferentes medios como las
interfaces, las clases abstractas, los flujos y los iteradores para realizar lo mejor posible la abstracción de los
objetos.
El desarrollo orientado a objetos

1. Especificaciones del software


La primera etapa de un desarrollo de software consiste en redactar un documento que describa, utilizando un
lenguaje comprensible por el cliente que lo ha solicitado y por los desarrolladores, las funcionalidades del
producto que se ha de realizar. Este documento es muy importante porque va a definir los límites del
programa, formalizando las necesidades, las exigencias y las restricciones. Definirá los diferentes tipos de
usuario y sus posibles interacciones en función de sus permisos, en forma de casos de usos. Durante la
redacción de este documento, es absolutamente necesario impregnarse de la cultura de la empresa y dialogar
con todos los actores implicados. Las especificaciones constituyen el elemento de base de la modelización.
Gracias al cuidado particular que se ponga en su realización, se podrá evitar en el futuro cualquier tipo de
discrepancias.

2. Presentación del ciclo en V


Un desarrollo de software se compone de varias fases que hay que completar con meticulosidad para identificar
correctamente una necesidad y no equivocarse respecto a la arquitectura utilizada para darle solución. Existen
varios modelos de desarrollo, y el ciclo en V que se presenta aquí procede del dominio industrial. Este concepto
no es específico de la programación orientada a objetos y se utiliza mucho. Su aplicación «literal» producirá un
resultado correcto, pero puede llegar a ser pesado de implementar.

En el ciclo en V, se parte de las necesidades del cliente (rama izquierda en la parte superior de la V) para
descender en tres etapas hasta la redacción del programa (base de la V):

A partir de las necesidades de cliente, especificar las funciones.

A partir de las funciones, especificar la arquitectura global.

A partir de la arquitectura global, especificar los módulos detallados.

La base de la V se dedica a la codificación del programa.

A continuación, la subida por la rama derecha se va a hacer en tres etapas, que confirmarán los objetivos de la
rama izquierda, a saber:

Validar cada módulo solo por los juegos de pruebas unitarias.

Validar las relaciones y comunicaciones entre módulos, por medio de las pruebas de integración.

Validar las funciones de la aplicación agregando todos estos módulos.

Para terminar, en la parte superior derecha de la V, nos centramos en la cuestión «¿Hemos llegado a satisfacer
a nuestro cliente?».

Teóricamente, cada etapa debe ser el objeto de documentos, que deben ser releídos y aceptados por el resto
de los desarrolladores del equipo. Pasar a la siguiente etapa solo debe ser posible si el documento -incluidos los
planes de pruebas- ha sido totalmente validado.

En función de los modos de organización de los oficinas de diseño, los recursos asignados e incluso de los
plazos especificados, el respeto escrupuloso del ciclo en V podría sufrir algunos contratiempos. Conservando el
espíritu de que un programa debe responder a las necesidades del cliente y ser muy fiable. El cuidado que se
toma en los pasos correctos depende de ello.

A continuación, se muestra un ejemplo concreto que ilustra el ciclo en V con la lista de acciones de cada etapa,
voluntariamente no exhaustiva para entender lo principal.

Especificar las necesidades:


«Soy restaurador y todos los días debo ofrecer un menú a mis clientes. Por lo tanto, necesito una
herramienta sencilla para introducir e imprimir mi lista, inspirándome eventualmente en menús
anteriores.»
Especificar las funciones:

Introducir un texto en un editor.

Guardar el texto en un directorio.

Cargar un texto existente.

Imprimir un texto.

Especificar la arquitectura:
Aplicación gráfica Windows de escritorio que permita leer y escribir en los archivos, un texto introducido
por el usuario. La impresión del texto utilizará las interfaces clásicas del sistema operativo.

Especificar los módulos:

Una ventana gráfica (también llamada vista) que permita la edición de un texto.

Una clase de negocio (también llamada modelo), principalmente con un objeto String que contenga
el texto.

Una clase que permita la carga y registro del modelo utilizando las funciones de gestión de archivos
del sistema.

Una clase que permita imprimir el modelo, utilizando las interfaces de impresión de Windows.

Codificación.

Validar los módulos:

Probar el comportamiento del modelo en condiciones normales y no normales.

Probar la carga del modelo desde una lectura de archivo en condiciones normales y no normales
(ejemplo: arrancar la llave USB que contiene el archivo durante su lectura).

etc.

Validar la arquitectura:

Verificar que el módulo de lectura-escritura de archivos interactúa con el modelo.

Verificar que el módulo de impresión interactúa con el modelo.

Verificar que los errores devueltos por los módulos llegan correctamente a la aplicación y se
muestran al usuario.

etc.

Validar las funciones:

Verificar que la aplicación permite al usuario seleccionar un archivo para leer, cuyo contenido se va a
copiar en el modelo.

Verificar que la aplicación permite guardar un modelo, asignándole un nombre.

Verificar que los errores de la aplicación se muestran de manera comprensible para todos.

etc.

Validar la aplicación:

Verificar que la aplicación es fácilmente accesible en la interfaz de Windows.

Verificar que el usuario puede fácilmente crear, recargar, modificar e imprimir menús.

Como regla general, la parte de análisis debe tener en cuenta las necesidades actuales del cliente, pero
también se debe anticipar a sus potenciales evoluciones. En el caso de nuestro restaurador, el dato principal es
el texto del menú. Por lo tanto, podríamos haber utilizado de manera central un objeto String que, como
veremos, es el objeto que encapsula las cadenas de caracteres en Java. Los módulos de la aplicación habrían
intercambiado, por supuesto de manera sencilla, las referencias sobre este texto y el objetivo fijado se habría
conseguido. Imaginemos que, un tiempo después de la puesta en servicio de esta primera versión, nuestro
restaurador vuelve a vernos y nos pide añadir una gestión de colores y de tipos de letra. Por lo tanto, la
información central se debería enriquecer y el String ya no sería conveniente. Se deberá sustituir por un objeto
que contenga el menú y toda su información de formateo. Todos los módulos codificados para intercambiar los
String también se deberán retocar para intercambiar este nuevo objeto, lo que sería una pena.

Conclusión: si desde el inicio ha reflexionado sobre la creación de un objeto de negocio, entonces se anticipará
a futuras evoluciones. Puede añadir nuevas propiedades sin tener que modificar la manera en la que los
módulos se comunican. En efecto, intercambiar un objeto de negocio que contiene una o diez propiedades se
codifica de la misma manera: se pasa su referencia (digamos su dirección única en memoria) y no su
contenido. A continuación, los módulos utilizarán las propiedades que necesitan, recorriendo el objeto por su
referencia. En un ejemplo sencillo como este, esto puede provocar gracia, pero en un proyecto importante, es
otra cosa.

Durante la redacción de los planes de prueba, hay que pensar en la utilización normal, pero especialmente en
el peor de los casos. Nunca confíe en los usuarios de sus módulos; compruebe siempre los argumentos
transmitidos. Parta desde el principio de que sus objetos se podrán inicializar incorrectamente, utilizarse mal,
configurarse mal, cerrarse incorrectamente, etc. Tenga el espíritu que hay detrás de esta cita de Leonardo da
Vinci: «Quien no predice lo lamentará».
En la mayor parte de los casos, será inútil escribir aplicaciones encargadas de probar sus objetos porque, como
veremos en el capítulo dedicado a este tema, existen entornos de pruebas que permiten comprobarlos
rápidamente.

Pero, en primer lugar, nos centramos en la modelización.

3. Modelización y representación UML


La modelización es la fase esencial del desarrollo de una aplicación. Se basa en las especificaciones y consiste
en analizar y descomponer un proceso en varios elementos sencillos. Posteriormente, permite «diseñar los
contornos» de los componentes que se han de realizar, comprobar si serán evolutivos, robustos, fiables y si sus
asociaciones realizarán el objetivo pedido. La modelización oculta los detalles para presentar lo principal.
Hablamos de la abstracción.

Entonces, se plantea la cuestión de una representación «normalizada» de la modelización, cuestión a la que el


lenguaje unificado de modelado (o UML, del inglés Unified Modeling Language) responde completamente.

Volvamos un poco atrás para ver cómo nace este lenguaje de modelización orientado a objetos que es el UML.

La explosión de la programación orientada a objetos y el crecimiento de la complejidad de los programas


condujeron a una multiplicación de los métodos orientados a objetos al inicio de los años 90. Entre otros,
podemos mencionar estos métodos:

Booch’91, de Grady Booch;

Object Modeling Technique (OMT), de James Rumbaugh, en 1991;

Object-Oriented Software Engineering (OOSE), de Ivar Jacobson, en 1992.

La representación, de manera estándar, del funcionamiento de un sistema, de la arquitectura y de la


comunicación de sus objetos rápidamente se convierte en algo necesario para:

Estructurar de manera evolutiva sus componentes.

Aumentar su fiabilidad y la seguridad de su conjunto.

Facilitar su mantenimiento.

Transmitir y asegurar su comprensión por parte de otros equipos.

Reutilizar sus componentes.

En 1997, con un objetivo de unificación, los diferentes métodos pusieron en común sus puntos fuertes,
validados por los retornos de experiencia verificados, para dar origen a un modo de representación
denominado Unified Modeling Language en su versión 1.0.

UML es un lenguaje de modelización orientado a objetos no propietario. Se rige por el Object


Management Group (OMG) y la norma está disponible gratuitamente en http://www.uml.org. UML es un
lenguaje gráfico, mientras que C++, Java y C# son lenguajes textuales.

Los diseñadores de software utilizan UML para representar sus modelos como imágenes gráficas, también
llamadas vistas. Estas vistas contienen los diagramas de diferentes tipos que explican, bajo diferentes ángulos,
el contenido y el funcionamiento de la aplicación. Algunas veces es necesario tener varias vistas para
representar y entender un modelo.

A continuación se muestran los nueve principales tipos de diagramas UML y sus objetivos:

Diagramas de caso de uso: describen los servicios ofrecidos por el sistema desde el punto de vista del
usuario. Estas vistas ponen en escena a los actores, que pueden ser humanos o representar otros
sistemas.

Diagramas de objetos: muestran el estado de una aplicación en un instante dado, enumerando las
instancias de las clases.

Diagramas de secuencias: muestran las interacciones entre los objetos durante la ejecución del
programa. El acento se pone en el orden de estas interacciones en esta representación temporal.

Diagramas de clases: capturan la estructura estática de la organización de las clases.

Diagramas de componentes: se corresponden con las vistas modulares de la aplicación que agrupan las
clases que colaboran.

Diagramas de despliegue: modelizan el aspecto del hardware de la aplicación.

Diagramas de colaboración: muestran cómo se organizan los objetos para trabajar en conjunto. El
acento se pone en las comunicaciones existentes entre los objetos.

Diagramas de estados-transiciones: representan el comportamiento de un objeto en forma de un


autómata de estados.

Diagramas de actividades: representan el flujo de ejecución de un proceso o de una operación.

La mayor parte de los desarrolladores solo utilizan un subconjunto del UML, principalmente los diagramas de
casos de uso, los diagramas de clases y los diagramas de secuencias.

Existe mucho software «libre» que permite construir diagramas UML, como StarUML, que se puede descargar
en la dirección: http://staruml.sourceforge.net/en/download.php
a. Los diagramas de casos de uso

La función de los diagramas de casos de uso es delimitar el perímetro de la aplicación, indicando sus
«actores» y los diferentes escenarios posibles que pueden reproducir en el sistema. Un caso de uso
representa un servicio funcional de la aplicación descrita en las especificaciones. El caso de uso se acompaña
de un texto que lo describe precisamente con sus condiciones de partida, su desarrollo normal y el resultado
de su ejecución. Para precisar el caso de uso, se pueden añadir diagramas de secuencias o diagramas de
actividades.

Esta ilustración representa cuatro actores que tienen permisos para realizar determinadas operaciones en un
sistema informático. Un actor un poco especial, Admin system, hereda de los otros tres y, por lo tanto, tiene
permisos para realizar sus operaciones.

b. Los diagramas de clase

La agrupación de los objetos de un mismo tipo permite factorizar sus atributos y sus comportamientos. La
representación gráfica se realiza en un diagrama de clases. En este diagrama es donde se definen los
componentes finales de la aplicación, por supuesto sin indicar el número de instancias. Las relaciones entre
las clases también se representan para cada tipo de relación, por medio de un signo gráfico diferente. El
conocimiento de las diferentes notaciones utilizadas es primordial para una transposición correcta a código
Java. Los diagramas de clases no presentan los aspectos dinámicos y temporales.

Una clase se representa por un rectángulo dividido verticalmente en tres partes:

En la parte superior: el nombre de la clase.

En el medio: los atributos de la clase (las variables).

En la parte inferior: los comportamientos de la clase (los métodos).

Los miembros de la clase (atributos y comportamientos) se preceden por un signo (+, #, -), que indica su
accesibilidad. Esta información permite administrar la encapsulación y la herencia descrita con anterioridad
en este capítulo.

El signo + indica que este miembro de la clase es accesible por todos y sin restricción.

El signo # indica que este miembro de la clase es accesible únicamente por sus subclases (clases heredadas).

El signo - indica que este miembro de la clase es privado y, por lo tanto, se utilizará únicamente por la clase
para su propio funcionamiento interno. Pequeña finalidad: un objeto puede acceder a los miembros privados
de otro objeto si los dos son del mismo tipo.

Después del signo de accesibilidad y del nombre del atributo, aparece un ’:’, seguido del tipo del atributo.
Este tipo puede ser «clásico» (entero, carácter, doble, etc.) o más complejo, como otra clase. Eventualmente,
el signo ’=’ seguido por un valor puede terminar la definición. Se trata del valor asignado al atributo durante
la instanciación. Veremos que, sin esta declaración, se inicializa un valor por defecto durante la instanciación
del objeto. Por ejemplo, un atributo de tipo entero se inicializará automáticamente a 0.

Si respeta la regla de la encapsulación, entonces ningún atributo debería aparecer precedido de un +.

De la misma manera que los atributos, los métodos de la clase también están precedidos por sus tipos de
acceso. Después del nombre del método, aparecerá la lista entre paréntesis de los argumentos con nombre y
tipo. Puede no haber argumentos. Para terminar, precedido por un ’:’, aparece el tipo de retorno. Si no se
devuelve ningún tipo, entonces se utiliza void.

Atención: esta sintaxis UML que define un comportamiento es diferente de la que utiliza Java cuando define el
método asociado.

Ejemplo:

En UML: +Addition(a:Int, b:Int):Int

En Java: public int Addition(int a, int b)

Recordemos que varios métodos pueden tener el mismo nombre. Esta forma de polimorfismo es posible si los
argumentos de los métodos con el mismo nombre son diferentes.

Las relaciones entre las clases

Las clases se pueden relacionar de manera más o menor fuerte y los diagramas representan gráficamente
esto con diferentes tonos.

Representación de una herencia

Esta especialización se representa en UML por una flecha, con un triángulo cerrado, que va desde la clase
derivada (subclase, clase heredada o incluso clase especializada) hasta la clase madre (superclase o clase de
base).

La clase heredada es la prolongación de la clase de base. El inicio de la porción de memoria asignado a una
clase heredada apunta a la clase de base; los aspectos específicos de la clase heredada llegan a continuación.
Gracias a esta organización de memoria, podemos decir que como una clase heredada es una especie de
clase de base, se puede utilizar en cualquier sitio donde se solicite su clase de base.

Por lo tanto, la clase heredada se beneficia de los miembros públicos (+) y protegidos (#) de la clase de
base.

Representación de una realización

Una clase puede implementar varias interfaces. Como veremos más adelante, esto consiste en soportar en la
clase una lista exhaustiva de métodos descritos en la interfaz. En este caso se habla de realización y el UML
lo representa con una línea punteada terminada por una flecha en forma de triángulo cerrado, dirigido hacia
la interfaz.
Representación de una relación sencilla (asociación)

Una clase puede contener una referencia a otra y, de esta manera, acceder a sus servicios. La representación
de esta asociación de navegación es una flecha abierta que apunta a la clase contenido. Las líneas del ciclo de
vida de las dos clases son independientes.

Representación de una relación bidireccional

Se pueden asociar dos tipos de clases durante una determinada operación. Estas dos clases no tienen
ninguna relación «parental»; sus líneas de ciclo de vida son totalmente independientes. Por ejemplo, una
clase Pedido contiene una lista de Articulos. No es el Pedido el que crea los Articulos. Si el pedido se elimina,
los Articulos permanecen.

Una restricción de cardinalidad puede precisar los términos de la asociación. En el esquema anterior, se debe
leer que un pedido contiene al menos un artículo y un número indefinido de artículos (1..*). En el otro
sentido, un artículo pertenece a un pedido. Se habla de índice de cardinalidad o multiplicidad.

Representación de una relación de tipo agregación

Una flecha con un rombo une las dos clases. El rombo se encuentra del lado de la clase «contenedor». Si el
rombo está vacío, quiere decir que los objetos creados por esta clase permanecerán cuando la clase
desaparezca.

Por ejemplo, un fabricante de reproductores digitales tendrá este tipo de relación con sus productos
fabricados. Incluso si «echa el cierre», los reproductores continuarán funcionado. Se habla de agregación por
referencia porque hay varios objetos independientes en memoria y determinados objetos memorizan las
referencias del resto.
Representación de una relación de tipo composición

Si el rombo está relleno, entonces la relación es muy fuerte. Retomemos el ejemplo de un hostal. Si el hostal
se destruye, entonces sus habitaciones también. Se habla de agregación compuesta o de agregación por
valor. Las duraciones del ciclo de vida de ambos son idénticas.

Las dependencias entre clases

Una parte del juego depende de las reglas de este juego. Aquí no hay relación de asociación o de
composición; sencillamente una dependencia entre el curso de la partida y las reglas escritas. UML
representa esta relación por una línea punteada.

c. Las enumeraciones

En programación, frecuentemente sucede que buscamos limitar los valores posibles de una variable a un
juego determinado de valores. Supongamos que necesita un tipo de datos que almacene un día de la semana
y nada más. Usted estará tranquilo si el compilador muestra un error en caso de que intentemos introducir
otra información diferente usando programación, como un mes del año, por ejemplo.

Esta seguridad es posible gracias a las enumeraciones, que permiten definir una lista de valores posibles.
Poco importa cómo el compilador va a codificar estos valores. Lo que cuenta es que prohíbe cualquier otro.

En UML una enumeración se representa como una clase para la que se prefija el nombre, usando
<<enumeration>>. A continuación se utiliza directamente para tipar los atributos de las clases que
necesitemos.

Este diagrama representa una clase Suscriptor que contiene varios atributos, entre los que hay un tipo
enumerado DiaDeLaSemana, que lista una serie de valores para cada día de la semana. Generalmente, los
compiladores asignan un juego de valores empezando por 0 y que va en progresión de uno en uno; pero
poco importa, porque en el código nunca se hará referencia a estos valores de sustitución (se podría producir
un error de compilación). El código utilizará sencillamente los elementos de la lista.
if( miCliente.DiaEntrenamiento == DiaDeLaSemana.Lunes)
{
//...
}

En casos muy particulares, puede ser interesante «fijar» en el código los valores asociados a las
enumeraciones. Java lo permite.

d. Los diagramas de secuencia

El diagrama de secuencias es una representación cronológica de las interacciones entre los objetos para la
realización de un caso de uso. Describe la lista de los objetos que intervienen, sus líneas del ciclo de vida y la
cronología de las interacciones.

A continuación se muestra un ejemplo de diagrama de secuencias. Representa el encadenamiento de las


interacciones que permiten la apertura de una puerta, después de reconocer la tarjeta de acceso del usuario.

Cada objeto que interviene se simboliza por medio de un rectángulo, que contiene el nombre de un actor o el
nombre de una instancia particular, seguida del nombre de la clase asociada. En este rectángulo se añade un
trazo vertical discontinuo, que representa la línea del ciclo de vida del objeto. Entre estas líneas del ciclo de
vida, se adjuntan flechas horizontales que representan las interacciones. Cada flecha se numera y se le
asigna un nombre. Este nombre generalmente se corresponde con un método, que se implementa en el
objeto receptor del mensaje. El secuenciamiento siempre empieza en la parte superior y normalmente a la
izquierda.

La interacción entre objetos

En el lenguaje UML, la interacción entre objetos se realiza por medio de mensajes. Concretamente, la mayor
parte de las veces estos mensajes son sencillas llamadas a métodos de las instancias de clase. Es el
comportamiento del emisor respecto al retorno el que va a especificar el tipo del mensaje e influir totalmente
en la codificación que se debe implementar.

En efecto, se habla de mensaje:

síncrono cuando el emisor espera la respuesta del receptor y bloquea la ejecución del programa,

asíncrono cuando la respuesta del receptor va a llegar de manera diferida al emisor.

Un objeto también puede enviar mensajes; se habla de interacción interna o de mensaje repetitivo.

La lectura del diagrama de secuencias permite conocer la naturaleza del mensaje intercambiado en un
momento dado. La representación gráfica de los mensajes en el diagrama de secuencias es la siguiente:
Observe la forma de la flecha que termina en el mensaje:

Flecha cerrada: mensaje síncrono.

Flecha abierta: mensaje asíncrono.

4. Codificación, integración y puesta en producción


La redacción del código se debe llevar a cabo respetando los fundamentos de la POO.

Algunos problemas aparecen de manera recurrente en programación. Antes de intentar inventar sus propias
soluciones, puede ser muy acertado mirar lo que han hecho otros desarrolladores para resolver problemas
parecidos. Los patrones de diseño (design patterns) describen las soluciones sencillas y verificadas en POO
para resolver estos problemas. Estos design patterns no son librerías de código, sino métodos para resolver el
problema; la implementación en un lenguaje dado es responsabilidad del desarrollador. A continuación, en este
libro, ofrecemos las implementaciones Java de dos patrones de diseño. Para ir más allá, puede conseguir el
catálogo de los design patterns «clásicos», llamado GoF por las iniciales de Gang of Four, término que designa
los cuatro coautores: Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides.

También es muy aconsejable realizar un control de calidad estricto a medida que se vayan construyendo las
clases. Los entornos de desarrollo actuales normalmente integran los proyectos, lo que comúnmente se llama
«framework de pruebas». El desarrollador utiliza este framework para escribir un juego de pruebas unitarias,
comprobando que sus objetos respondan a las especificaciones de diseño, así como que no hayan sufrido
regresiones durante las operaciones de mantenimiento o las evoluciones «de último minuto». Estas pruebas se
podrán lanzar automáticamente después de la compilación y la propia herramienta de desarrollo formateará los
resultados.

Después de estas pruebas unitarias, vienen las pruebas de integración, que permiten comprobar que los
módulos se comunican correctamente entre ellos. Estas pruebas también se pueden automatizar en el entorno
de desarrollo.

A continuación, la aplicación se puede probar en su conjunto durante el desarrollo de un plan de pruebas


concreto. Pero como este conjunto se constituye con módulos ya comprobados por las pruebas unitarias y de
integración, los funcionamientos incorrectos residuales deberían ser más del tipo ajuste que correcciones de
errores importantes.

Esta última fase de puesta a punto prefija el ajuste del producto. Esta fase de ajuste es una serie de pruebas
sobre un escenario escrito y validado por el cliente y el desarrollador para controlar que el software está
conforme a las especificaciones.
Ejercicios

1. Jerarquía de clases

Enunciado

Diseñe el diagrama de clases UML que resuma las relaciones entre los siguientes objetos:

Helicóptero

Submarino

Moto

Transporte terrestre

Camión

Coche

Transporte

Avión

Scooter

Paquebot

Transporte aéreo

Camión cisterna

De dos ruedas

Coche descapotable

Transporte marítimo

Avión de combate

Moto de cross

Bicicleta

BTT

De cuatro ruedas

Corrección

En esta jerarquía de clases aparece una clase general de base, que es Transporte. A continuación, esta clase
de base se va a especializar a medida que se avanza en diferentes niveles. Tenga en mente la relación «es una
especie de...», que ayuda a entender las nociones de herencia y polimorfismo. La BTT especializa su padre
Bicicleta, añadiendo las funciones de recorrido todo terreno, pero conservando sus atributos de base: es la
herencia. La BTT es «una especie de» bicicleta que, a su vez, es «una especie de» transporte terrestre de dos
ruedas que, finalmente, es «una especie de» transporte. En consecuencia, cuando se solicita un objeto
Transporte, entonces se puede utilizar una BTT: es el polimorfismo.

2. Relaciones entre objetos


Enunciado

En un proyecto de gestión de librerías, se desarrollan los objetos que representan a los libros y los objetos que
representan a los autores. Diseñe su diagrama de clases con los atributos mínimos y sus relaciones.

Corrección

En esta relación bidireccional, un autor ha escrito un número indefinido de libros, pero al menos uno (1..n). Un
libro está escrito por un autor (1).

3. Agregación de objetos

Enunciado

Diseñe el tipo de agregación existente entre:

una pared y su papel pintado,

una habitación y sus muebles,

una calle y sus aceras,

un libro y su encuadernación,

un libro y sus páginas.

Corrección

Las paredes se recubren de papeles pintados y, por lo tanto, los papeles pintados están relacionados (y
pegados) a las paredes. Se trata de una relación fuerte porque, si se destruye la pared, entonces los papeles
pintados se destruirán. El rombo está lleno.

La habitación contiene muebles. La relación entre muebles y habitación es débil porque, si tomamos la
precaución de sacar los muebles antes de destruir la habitación, entonces podrán seguir existiendo. El rombo
está vacío.

La calle y sus aceras tienen una relación fuerte. Si desaparece la calle, las aceras dejarán de existir. El rombo
está lleno.

Libro y encuadernación también están fuertemente relacionados. La encuadernación no sobrevivirá a la


destrucción del libro. El rombo está lleno.

Libro y páginas están débilmente relacionados. La página arrancada que contiene el poema encontrará un lugar
en una carpeta cuando el libro se destruya: el rombo está vacío.
Así se analiza el ciclo de vida de sus objetos. Según su naturaleza y las funcionalidades de sus aplicaciones, se
crearán, colaborarán y, a continuación, desaparecerán. Algunos se destruirán antes que otros. Otros implicarán
destrucciones masivas de sus atributos fuertemente relacionados.

4. Diagrama de casos de uso

Enunciado

¿Cuáles son los actores que intervienen en una compra en Internet?

Represente los casos de uso relacionados con una compra en Internet.

Corrección

Los actores principales son:

el internauta comprador,

el banco,

el vendedor,

el repartidor.

Recuerde que un actor no es obligatoriamente una persona física.

Los use cases o casos de uso resumen lo que hacen los actores en el sistema. Deben permanecer muy sencillos
y legibles.

5. Diagrama de secuencias

Enunciado

Diseñe el diagrama de secuencias correspondiente a la compra en Internet.

Corrección
Aquí, se simbolizan los intercambios entre los objetos y la duración de su ciclo de vida. Observe que podíamos
haber tenido diferentes variantes de nuestro caso. Este resumen permite entender los encadenamientos de
acciones y los retornos. Los mensajes con flechas completas implican una respuesta inmediata, mientras que
las flechas abiertas significan un retorno diferido. Los mensajes normalmente llaman a los métodos de objeto.
Por lo tanto, el retorno síncrono (inmediato) será el retorno del método, mientras que el retorno asíncrono
(diferido) utilizará mecanismos de notificación más elaborados, que estudiaremos más adelante.
3. INTRODUCCIÓN A LA PLATAFORMA JAVA
Introducción
En el corazón de los ordenadores, los microprocesadores ejecutan las listas de instrucciones. Estas listas
contienen códigos binarios que representan cálculos y acciones que se tienen que hacer sobre los registros, la
memoria o incluso los periféricos. Cuando un desarrollador escribe un programa, no construye directamente esta
lista. Por fortuna, pasa por un lenguaje de programación con el que va a describir una sucesión de acciones de
«alto nivel». Por ejemplo, si el programa necesita abrir el archivo config.txt, entonces el desarrollador utilizará la
función fopen(«config.txt») de su lenguaje favorito para hacerlo. Después de esta línea, se ocultará toda una
lista de instrucciones «de máquina» que el microprocesador ejecutará cuando la aplicación se ponga en marcha.
Por lo tanto, los lenguajes simplifican mucho la escritura, pero, al final, siempre son instrucciones de máquina las
que se ejecutan.

El programa «fuente» -en nuestro ejemplo el que contiene fopen(«config.txt»)- es un archivo de texto que el
desarrollador edita con un editor de texto básico, como Notepad, o más evolucionado, como Notepad++ o
incluso el editor integrado en su entorno de desarrollo (IDE), que se presentará más adelante. La máquina no
puede ejecutar este archivo de texto. Se debe convertir directa o indirectamente en lenguaje «máquina» para
que sea ejecutable.

Esta translación se puede realizar de tres maneras diferentes:

La compilación directa: el archivo se convierte en un contenido binario directamente ejecutable por el


sistema operativo. Es el caso de la mayor parte de los programas escritos en C y C++ (la «mayor parte»
porque es posible escribir programas .NET en C++... pero es otra historia).

La interpretación: un intérprete convierte el archivo «sobre la marcha». Es el caso de los scripts PHP en
las páginas web. El navegador de Internet del cliente solicita una página a un servidor web. El servidor
web detecta que la página solicitada contiene un script PHP. Entonces la pasa a su analizador, que va a
interpretarla y convertirla dinámicamente en página HTML. Este método es muy práctico en el caso de la
Web porque los scripts se escriben en formato texto. En caso de modificaciones, no es necesario
recompilar nada. Es suficiente con actualizar el archivo en la arborescencia del servidor y el cambio es
efectivo inmediatamente. Por supuesto, el conjunto (decodificación del PHP, su ejecución y recodificación
en HTML) puede ser lento.

La compilación indirecta: es el caso de Java y de C# y es una mezcla de los dos... En efecto, el archivo
fuente se compila en un formato binario intermedio, que se va a ejecutar muy rápidamente por una
máquina virtual. ¿Para qué situar esta máquina virtual entre el programa y el sistema operativo? Esta
máquina virtual sirve para varias cosas: en primer lugar, va a comprobar que el programa fuente no haga
«tonterías» durante su ejecución, escribiendo por ejemplo en la memoria de otras aplicaciones y, de esta
manera, haciendo que el sistema operativo se vuelva inestable. También se va a ocupar de la gestión de
la memoria, la seguridad, los permisos, etc. La máquina virtual es la mejor amiga de los sistemas
operativos y los desarrolladores. Por un lado, asegura una ejecución de máquina lo más fiable posible y,
por otro, genera un código fuente (prácticamente) único para todas las plataformas.

Microsoft Windows ofrece el framework .NET y su máquina virtual. .NET se programa con muchos lenguajes,
incluso C# se diseñó para él. Sin embargo, este potente framework está muy relacionado con los sistemas
operativos de Microsoft (PC, servidores, Web, tabletas, teléfonos), incluso si MONO es una implementación .NET
para Linux y Xamarin generaliza C# para desarrollar en Windows, Linux (Android) y iOS (Apple). Este esfuerzo
de apertura de Microsoft es relativamente reciente, mientras que, desde el inicio, Java se ha podido ejecutar en
la mayor parte de los sistemas operativos que ofrecen una máquina virtual apropiada.

Por lo tanto, una plataforma Java designa un entorno de ejecución para un sistema operativo en una
máquina dada, con el entorno de desarrollo asociado.
Entorno de ejecución
Las aplicaciones escritas en Java no se comunican nunca directamente con el sistema operativo. Además, el
resultado de una compilación Java (archivos con extensión .class) no es directamente ejecutable por el sistema
operativo. Esta primera compilación, llamada Byte Code, contiene las instrucciones para la máquina virtual de
Java, que va a «convertir sobre la marcha» el código intermedio en instrucciones compatibles con el entorno real
de ejecución. Se habla de Just In Time Compiler (o JIT Compiler).

Cada entorno real de ejecución (Windows, Linux, macOS...) tiene su propia máquina virtual de Java, pero el Byte
Code generado después de la compilación del programa fuente es el mismo y relativamente portable entre
diferentes máquinas virtuales de Java. El adverbio «relativamente» se añade aquí para precisar que el diseño de
un programa «portable» requiere algunas precauciones. Imaginemos que desea realizar un programa que
deberá funcionar al mismo tiempo en el PC, la tableta y un teléfono. Es evidente que las características de estos
tres dispositivos son totalmente diferentes y, por lo tanto, será necesario que el programa se adapte
automáticamente a las características del entorno real. Por ejemplo, podrá solicitar la resolución del dispositivo
de visualización para adaptar su presentación en consecuencia.

La otra cara de la moneda: el resultado de la compilación se puede descompilar fácilmente, es decir,


convertir el Byte Code en líneas de programa fuente Java.

Esto es un problema cuando, por ejemplo, su competencia consigue reconstruir su código fuente a partir del
producto «compilado» que usted comercializa... Es posible que el resultado de esta acción sea menos legible,
realizando una operación de ofuscación con herramientas especializadas, como ProGuard (open source). Sepa
que el problema es el mismo en C#...
Una librería muy completa
La plataforma Java ofrece una amplia colección de clases sobre las que se basan las aplicaciones. Se habla de
API Java. Estas clases simplifican considerablemente la gestión de los objetos habituales (cadenas de caracteres,
valores decimales, colecciones, etc.), así como la gestión de archivos, las interfaces gráficas clásicas, los API
web, el acceso a las bases de datos, las comunicaciones de red, la seguridad, los diagnósticos, etc. La lista de las
clases es muy amplia y, aunque se ofrezcan de manera jerárquica, el problema normalmente es saber
identificarlas.

La mayor parte de las veces, estas clases son extensibles. Esto quiere decir que es posible extenderlas para
mejorar su comportamiento básico y, a continuación, añadir los aspectos específicos del negocio.

Para organizar estas clases, la plataforma utiliza el concepto de paquetes, que agrupa las clases por objetivos
(servicios de la misma naturaleza). Por ejemplo, el paquete java.io contiene una «caja de herramientas» para
administrar los archivos.

El API (Application Programming Interface, término que designa la lista de las clases accesibles por el
desarrollador) básico de Java 9 ofrece no menos de 6000 clases organizadas en más de 200 paquetes.

Naturalmente, es posible crear sus propios paquetes. El paquete Java se corresponde con el paquete del UML. El
desacoplamiento realizado intenta reducir las dependencias entre los paquetes. Para que un programa pueda
utilizar un paquete, este último se debe referenciar en el proyecto y su utilización se debe declarar en la parte
superior del archivo fuente implicado mediante la directiva import, por ejemplo: import java.io.*;.
Las herramientas de desarrollo con buen rendimiento
La plataforma Java ofrece un compilador (javac.exe), que se puede utilizar directamente en línea de comandos,
después de haber introducido el código del programa en un editor de texto. Por supuesto, el procedimiento es
posible pero no muy productivo. Evidentemente, el desarrollador persigue la utilización de un entorno de
desarrollo totalmente integrado, que le ayude durante la redacción del código, la realización de la interfaz
gráfica, la puesta a punto y el despliegue de su aplicación. Este programa es un IDE (Integrated Development
Environment). Como mínimo, integra un editor de código fuente, las herramientas que automatizan las
compilaciones y un depurador.

Existen varios IDE para Java. Entre ellos hay tres principales: IntelliJ IDEA, de JetBrains; Eclipse, de la fundación
Eclipse, y NetBeans, creado por iniciativa de Sun Microsystems y actualmente mantenido y distribuido por
Oracle. Las demostraciones y ejercicios de este libro se basan en la utilización de IntelliJ IDEA porque es el IDE
que nos ha parecido más sencillo para empezar a desarrollar.
Descarga e instalación de IntelliJ IDEA
Escriba en la barra de direcciones de su navegador de Internet:
https://www.jetbrains.com/idea/download/#section=windows

Se muestra la siguiente pantalla.

El IDE se ofrece en dos versiones. Vamos a utilizar la versión «Community», que será suficiente para
nuestros experimentos. Por lo tanto, haga clic en el botón Download de Community.

Empieza la descarga.

Una vez que termina la descarga, haga doble clic en el archivo descargado.

Permita las modificaciones para esta aplicación.

Haga clic en el botón Next y conserve los valores por defecto propuestos en las siguientes pantallas.
El inevitable Hello World
Incluso si el aprendizaje de IntelliJ IDEA se hace al mismo tiempo que la POO con Java, no nos resistimos más a
redactar nuestro primer programa: el clásico Hello World!

Ejecute IntelliJ IDEA desde el menú Inicio - JetBrains - IntelliJ IDEA Community Edition.

Seleccione el tema (colores de la aplicación) que quiera.

La siguiente pantalla permite seleccionar los plug-ins por defecto. Los plug-ins son componentes activos que se
pueden instalar en el IDE. IntelliJ IDEA es como su caja de herramientas, en la que mete las herramientas (plug-
ins) que utiliza más habitualmente. Es inútil ocupar espacio en la caja con un avión de madera si la mayor parte
de su tiempo lo pasa haciendo mecánica. Aquí sucede algo parecido, por lo que vamos a modificar la distribución
por defecto, validando Swing para nuestras futuras experiencias gráficas.
Conserve las opciones por defecto de la siguiente pantalla:

Haga clic en Start using IntelliJ IDEA.


Una vez se carga la pantalla de bienvenida de IntelliJ IDEA, haga clic en la opción Create New Project.

Esta acción ejecuta el asistente de creación de aplicaciones, que va a comprobar si están todas las herramientas
necesarias y va a preparar el esqueleto de nuestra futura aplicación.

En esta primera pantalla, el IDE nos informa de que todavía no se ha instalado ningún SDK. En efecto, IntelliJ
IDEA no contiene los componentes Java (como los compiladores) que mantiene y ofrece la empresa Oracle.
Estos componentes se distribuyen como kits de desarrollo de software o Software Development Kit (SDK). En el
caso de Java, trabajamos con los Java Development Kit (JDK) e IntelliJ IDEA nos ofrece descargarlos e
instalarlos. Durante esta fase de instalación, es posible que tenga que permitir que su cortafuegos de Windows
desbloquee las funcionalidades de IntelliJ IDEA.

Haga clic en Download JDK.


Esta acción nos redirige a la página de las descargas Java del sitio web de la empresa Oracle.

Haga clic en la imagen DOWNLOAD Java Platform (JDK) 9 y, a continuación, acepte las condiciones de
utilización después de haberlas revisado.

En la lista propuesta de JDK, haga clic en el correspondiente a Windows para arrancar la descarga.

Pida la ejecución del programa descargado.


Una vez que termine la descarga, permita las modificaciones que el asistente de instalación realizará en su
sistema.

Se muestra el asistente de instalación del JDK en la primera pantalla; haga clic en el botón Next.

Conserve los valores por defecto y haga clic en Next para lanzar la instalación.

Después de algunos segundos de copia de archivos, haga clic en el botón Siguiente de la pantalla de
instalación personalizada.
La instalación continúa durante varios minutos.

Para terminar, haga clic en el botón Close de la pantalla final.

De vuelta a IntelliJ IDEA, haga clic en el botón New en la parte superior derecha para indicar el directorio
del JDK que se acaba de instalar. Por defecto, se encuentra en C:\Program Files\Java\jdk-9.0.1.
Ahora, la configuración del JDK se debe mostrar como sigue:

Haga clic en el botón Next.

En la siguiente pantalla, marque Create project from template. Esto va a permitir tener una estructura
básica de aplicación de tipo consola, lista para completarse. Haga clic en el botón Next.
Llame el proyecto helloworld, guarde el directorio por defecto e indique com.eni como nombre del paquete.
Para terminar, haga clic en el botón Finish.
Una vez que se ha preparado el proyecto, aparece la caja Tip of the Day. Es una manera amistosa de
enseñarnos un nuevo «truco» para el IDE en cada una de sus cargas.

Haga clic en el botón Close.

La parte izquierda del IDE muestra el Explorador de proyectos con su único proyecto, HelloWorld:

IntelliJ IDEA muestra, en la parte derecha, el contenido del archivo fuente único de nuestra solución: main.java.

La extensión .java se utiliza para los archivos de texto que contienen el código de la aplicación.
Complete el contenido de main.java como sigue:

Sin duda, habrá observado que, durante la redacción de esta línea, se muestra una ayuda contextual. Esta
funcionalidad es muy útil y comprobará rápidamente que será su aliada durante las fases de redacción de su
código. Capaz de «completar» su escritura, también ofrece una ayuda sobre la utilización del método.

Compile el programa con [Ctrl][Shift][F9].

Ejecute el programa con [Shift][F10].

Objetivo alcanzado: se muestra una ventana en la parte inferior de las otras dos, con el famoso «Hello World».

También es posible ejecutar esta primera «superaplicación» desde la consola Windows.

Para ello, siga este procedimiento:

Abra una consola Windows.

Vaya al directorio de salida utilizado por este proyecto, que por defecto es C:\Users\<su
nombre>\IdeaProjects\helloworld\out\production\ helloworld>.

Ejecute la aplicación validando el comando java com.eni/Main.


Observe que los argumentos del ejecutable java.exe son sensibles a la diferencia entre las mayúsculas y las
minúsculas.

En efecto, la clase Main forma parte del package com.eni; por lo tanto, para ejecutarlo desde el directorio de las
clases compiladas, hay que respetar la diferencia entre las mayúsculas y las minúsculas en el conjunto de los
dos nombres.

A continuación se muestra lo que sucede cuando no se respeta la diferencia entre las mayúsculas y las
minúsculas:

Observe también que, entre el nombre del paquete y el nombre de la clase, aparece un slash /. No se debe
confundir con su amigo el antislash \ habitualmente utilizado para navegar entre los directorios de Windows. El
slash permite al intérprete navegar en los paquetes para apuntar a la clase deseada.

Al contrario de C, C++ y C#, no se genera ningún archivo .exe. El ejecutable es java.exe, que es al que se
pasa la clase que contiene el punto de entrada.

En adelante, sus aplicaciones van a contener más de una clase y, por lo tanto, más de un archivo .class.
Entonces, puede agruparlas en una especie de sobre, que es el archivo .jar.

Para que IntelliJ IDEA construya este .JAR para nosotros:

Seleccione la opción Project Structure del menú File.

Seleccione Artifacts.

Haga clic en el botón +.

Seleccione JAR - From modules with dependencies.


Seleccione la Main Class llamada Main y haga clic en OK.

Deje las opciones por defecto y haga clic en OK.

Haga clic de nuevo en OK.


De vuelta a la interfaz principal, hay que seleccionar la opción Build Artifacts del menú Build.

La ejecución de un .jar desde el directorio C:\Users\<su nombre>\


IdeaProjects\helloworld\out\artifacts\helloworld_jar se realiza con el comando java -jar "<nombre del
.jar>".
Ejemplo

java -jar "helloworld.jar"

Puede recorrer el contenido del .jar sencillamente cambiando su extensión por .zip y cargándolo con un
editor de archivos comprimido, como el explorador de Windows 10.
Una puntualización sobre los acrónimos
Hagamos una pequeña puntualización sobre los acrónimos más utilizados en el mundo Java.

Hemos descargado e instalado el JDK 9. JDK significa Java Development Kit y es un SDK (Software Development
Kit) especial de Java, que contiene herramientas como el compilador, el depurador y también el JRE.

Hemos utilizado, sin saberlo, este JRE, que es el acrónimo de Java Runtime Environment. El JRE nos ha
permitido mostrar este triunfal «Hello World». En nuestro caso, el JRE estaba incorporado en el JDK, pero se
puede descargar e instalar de manera aislada.

Java es un potente lenguaje de desarrollo que permite crear aplicaciones para diferentes tipos de plataformas de
ejecución: las pequeñas máquinas sin mucha potencia, las máquinas de escritorio y, para terminar, las máquinas
que actúan como servidores con muchos recursos. A cada uno de estos tipos le corresponde una especificación
de lo que debe ofrecer la plataforma Java. Estas especificaciones son, respectivamente, JME (Java Micro Edition),
JSE (Java Standard Edition) y JEE (Java Enterprise Edition). Para el desarrollo, utilizamos las implementaciones
de estas especificaciones por parte de editores como Oracle. Por ejemplo, el JDK 9 que hemos utilizado se
corresponde con la implementación de las especificaciones del JSE por Oracle.

Para terminar, las especificaciones se indican en los JSR. JSR significa Java Specification Requests.
4. LOS TIPOS EN JAVA
Introducción
Recodemos que todo programa utiliza variables. La variable es un contenedor de memoria donde se almacena
información. El programa lee o escribe en sus variables según sus operaciones. La naturaleza de la variable
condiciona la información que puede contener. Por ejemplo, una variable de tipo sencillo puede contener una
información «booleana» 1 o 0 para sí o no. Una variable más compleja puede definir una persona con varios
«atributos», a su vez de tipos diferentes (cadenas de caracteres, fechas, valores digitales, etc.). Con Java (como
sucede con C, C++ o C#), una variable se debe declarar con un tipo que conservará durante todo su ciclo de
vida en el interior de la aplicación. La variable también debe tener un nombre. El compilador comprueba sus
instrucciones en función del tipo de las variables implicadas y devuelve error si intenta hacer operaciones
imposibles, como escribir información de una persona en una variable booleana...

En Java, la noción de variable normalmente está muy relacionada con las instancias de objetos, principalmente
con sus «campos» o «estados» (atributos en sentido UML). Las variables también se intercambian durante las
llamadas a los métodos e incluso se crean temporalmente para las necesidades de estas operaciones.

Hay dos tipos de variables:

los tipos primitivos,

los tipos por referencia.


Los tipos primitivos
A los tipos primitivos solo les falta la etiqueta 100 % Objeto para Java. Son «justo» contenedores de tamaños
específicos que almacenan los valores «primitivos» y no tienen métodos. Los tipos «primitivos» incluyen los ocho
tipos básicos presentes en la siguiente tabla:

Tipo Tamaño en bits Gama de valores

boolean Depende del sistema true o false

char 16 bits 0 a 65535

byte 8 bits -128 a 127

short 16 bits -32768 a 32767

int 32 bits -2^31 a 2^31-1

long 64 bits -2^63 a 2^63-1

float 32 bits -3.40282347E+38 a 3.40282347E+38

double 64 bits -1.79769313486231570E+308 a


1.79769313486231570E+308

Más adelante vamos a ver que todos los objetos heredan de la misma clase raíz java.lang.Object. Esta herencia
implícita puede ser muy práctica porque ofrece al desarrollador un juego de métodos básico común a todas las
instancias de objetos. Como es una lástima que los tipos primitivos no tengan esta funcionalidad, Java nos
ofrece las clases llamadas wrappers, que encapsulan cada tipo primitivo. Estas clases forman parte del paquete
java.lang y encontrar sus nombres es muy sencillo: la mayor parte de las veces, es suficiente con tomar el tipo
primitivo y pasar su primera letra a mayúsculas. De esta manera, se encapsula el tipo primitivo boolean en una
clase Boolean. Sin embargo, el tipo int se encapsula por Integer.

Vamos a estudiar la utilidad de la herencia de la clase raíz java.lang.Object en algunas líneas, pero ya podemos
experimentar las dos sintaxis.

A continuación se muestra una clase que contiene dos campos, de tipo boolean y de tipo Boolean,
respectivamente. Esta clase contiene un método Prueba que hace algunas operaciones muy básicas sobre estos
dos campos.

public class PruebaPrimitivoYWrapper {


boolean b1;
Boolean b2;

void Prueba()
{
// b1 es de tipo primitivo boolean
b1 = true;
// b2 es una referencia a un objeto de tipo Boolean
b2 = b1;
// Además de contener un valor,
// ofrece métodos.
System.out.println(b2.toString());
}
}

El objeto b2 de tipo Boolean encapsula el tipo primitivo boolean. Puede almacenar el valor true (o false) y
además ofrece métodos como toString, que como veremos devuelven una cadena representativa del objeto. En
este contexto System.out.println(b2.toString()); muestra true en la consola Java.

Los wrappers sobre los tipos primitivos no necesitan ser instanciados por el operador new. Los wrappers se
deben considerar como tipos primitivos con algunos métodos, pero no referencias como las que veremos
en el siguiente capítulo.

Nota: en C# también existe esta noción de tipos primitivos y de clases que las encapsulan. La diferencia con
Java es que estas clases wrappers se instancian automáticamente cuando el desarrollador llama a un método;
en otras palabras, b1.toString(); se habría admitido en el código anterior.

La duración del ciclo de vida de un tipo primitivo es la llave «padre».

Si una clase contiene diferentes estados almacenados en los tipos primitivos, entonces la duración del ciclo de
vida de estos últimos seguirá a la de la clase (definida entre llaves).

Si un método necesita un juego de variables de tipo primitivo para realizar su operación, entonces estas
variables desaparecerán tan pronto como se salga del método (definido por el paréntesis de cierre). A este
efecto, recordemos que los argumentos de un método son variables locales a este mismo método. Van a
desaparecer en el mismo momento que las que se declaran después del paréntesis de apertura.
Los tipos por referencia
Al contrario de lo que sucede con los tipos primitivos, los tipos por referencia almacenan las referencias a los
datos. Estos datos se escriben en una zona de memoria llamada heap (pila). Es accesible desde otras instancias
de clase. Su ciclo de vida termina cuando no se necesiten más. Mientras exista al menos una referencia activa en
la zona de datos, esta se mantendrá. Tan pronto como no haya más referencias, la zona se considera no
utilizada y se procede a su destrucción por parte del garbage collector.

Un tipo por referencia puede no referenciar a nada (o no todavía). En este caso, permanece como null.

La instanciación de una clase se realiza únicamente con la palabra clave new.

Una variable de tipo por referencia caracteriza una instancia de clase; a saber, la dirección donde está el objeto.
Como habíamos visto, el objeto mezcla atributos y métodos y, por lo tanto, incluye datos más complejos que
aquellos contenidos por los tipos primitivos.

Tendremos la ocasión de volver sobre este tema de las referencias y profundizar echando un vistazo a algunas
reglas básicas.

Una variable de tipo por referencia contiene la dirección de un objeto. Mientras el objeto no se asigne
explícitamente en su programa, es decir, que el sistema operativo no le asigne una fracción de memoria a su
tamaño, la variable contiene null.

Durante una operación de asignación como esta, son las referencias las que se duplican, y no los objetos.

// Creación de una variable por referencia llamada pr1


// sobre un objeto de tipo PruebaReferencia
PruebaReferencia pr1; // por el momento pr1 vale null
// Asignación de un objeto y almacenamiento de su dirección
// (por lo tanto, de su referencia) en la variable pr1
pr1 = new PruebaReferencia();

// Creación de una variable por referencia llamada pr2
// sobre un objeto de tipo PruebaReferencia
PruebaReferencia pr2; // por el momento pr2 vale null
// Copia en pr2 el contenido de pr1, por lo tanto, la dirección del objeto
pr2 = pr1;

Al final de este código, hay dos referencias que apuntan a un mismo objeto en memoria.

Durante una prueba de igualdad entre dos variables de tipo por referencia, son las direcciones de los objetos lo
que se comparan, y no el contenido de los objetos en sí mismos.

Cuando se utiliza una referencia como argumento de una operación (método de un objeto), es la dirección del
objeto lo que se pasa, y no el objeto en sí mismo. Por lo tanto, la dirección se comparte entre el programa que
llama al método y el método en sí mismo.

Cuando se utiliza una referencia como tipo de retorno de una operación, es lo mismo. Es la dirección del objeto
la que se intercambia.
Para ayudarnos...
Durante este paseo iniciático en el dominio de la POO, vamos a construir pequeñas aplicaciones de prueba en
modo consola. Usando la programación, vamos a comprobar determinada información y visualizar mensajes en
la ventana de salida Output de IntelliJ IDEA.

En lenguaje Java, la palabra clave assert permite comprobar si una condición es verdadera durante la ejecución
de su código. Utilizando este método, no interviene sobre el desarrollo del programa propiamente dicho;
comprueba que lo que está previsto en un determinado lugar del código es cierto. Si la condición es falsa, se
mostrará un mensaje de error para informarle de esto.

Un assert no es una operación condicional que deriva el flujo hacia uno u otro subprograma. Cuando un
assert se «despierta», debería ponerle sobre la pista de un defecto (bug) en su programa.

Sintaxis de utilización de la palabra clave assert

assert( <condición> );

Ejemplo de utilización de la palabra clave assert

Para comprobar la utilización de la palabra clave assert, vamos a retomar el proyecto HelloWorld y añadirle
algunas líneas.

public static void main(String[] args) {


// El fragmento obligado...
System.out.println("Hello World");

// Comprobación de la condición "1 es diferente de 2"
assert(1 != 2);
// Como la condición es verdadera,
// el programa pasa a la siguiente línea


// Para visualizar el resultado del comando
// cuando no se comprueba una condición,
// se "fuerza" un error en la siguiente línea
assert(1 == 2);


}

Antes de ejecutar la aplicación, vamos a añadir un argumento que se pasa durante la llamada a la máquina
virtual, que significa que debe tratar los assert porque, por defecto, la máquina virtual Java no trata la palabra
clave assert.

Haga clic con el botón derecho del ratón en el proyecto y seleccione la opción Properties del menú
contextual. Acceda a la opción Edit Configurations del menú Run.

En la pestaña Configuration, indique -ea en VM options.


Haga clic en OK y ejecute la aplicación con [Shift][F10].

En la ventana Output, un mensaje le informa de que en la línea 18 la condición que queríamos comprobar es
falsa.

En efecto, 1 no es igual a 2. Pensará que era previsible... Sí, pero la condición puede ser mucho más compleja
que esto. Imagine que ha diseñado una fracción de programa que solo se debe llamar si el código cliente
empieza por 240500 y la cantidad de la transacción es superior a 1000 €. Situando en la entrada de la operación
un assert con esta condición, se asegura -al menos durante la puesta a punto- de que su programa se llamará
en los límites de la condición y lo que haya previsto se realiza correctamente.

Se valida (opción -ae vista anterioridad) la operación del assert principalmente durante las fases de puesta a
punto. Si durante esta fase se llama al assert muy habitualmente, habrá que sustituirlo por un código de
protección que inhiba la operación si la condición no es verdadera.

La utilización de assert se recomienda durante la redacción de su código. Se trata de un primer nivel de


comprobación. Como se ha explicado anteriormente, un ciclo de desarrollo de aplicación debe integrar las
pruebas en diferentes etapas (pruebas unitarias, pruebas de integración, etc.) y no es suficiente con
contentarse con sencillas llamadas a assert, sino construir auténticos procedimientos de prueba de sus clases
con un framework especial, como JUnit.

Para visualizar los mensajes en la ventana Output, utilizamos System.out.println. Incluso si las nociones de
paquetes, clases y métodos todavía no están claras, digamos que utilizamos el método println de la clase out
que forma parte del paquete System. La clase out contiene muchos otros métodos que permiten visualizar en la
consola directamente datos numéricos, cadenas formateadas, caracteres, etc.
La superclase java.lang.Object
La clase Object es la clase «raíz» (clase de base) de las clases Java existentes y de las clases que va a crear (la
noción de herencia ya se ha abordado un poco en los primeros capítulos). La herencia de Object es implícita y,
por lo tanto, su declaración es inútil. Todas las clases heredan de sus métodos y la idea es sustituirlas
adaptándolas a la lógica de la clase que va a desarrollar. Por ejemplo, en la clase Object, existe un método
toString que devuelve una cadena «que representa al objeto». El hecho de implementar este método en su clase
va a «desconectar» el de la clase de base y le va a permitir reenviar una cadena que describa su objeto en la
forma que quiera. Por ejemplo, si su aplicación administra una lista de objetos de tipo Persona, entonces puede
construir en el método toString una cadena que retoma los atributos principales de cada instancia, como el
nombre, el identificador, etc., y llamará este método durante las fases de puesta a punto para visualizar la
información en la ventana Consola.

Vemos los métodos de java.lang.Object que vamos a poder sustituir o utilizar en nuestras propias clases.

1. equals

public boolean equals(Object o);

El funcionamiento básico de este método (por lo tanto, el comportamiento ejecutado si no lo redefine en su


clase) es análogo al del operador ==. Por lo tanto, «en la parte inferior», la prueba más básica es la
comparación de las referencias (finalmente las direcciones) de los dos objetos. Para comprobarlo, vamos a
poner en acción la palabra clave assert.

// Creación de una variable por referencia llamada pr1


// sobre un objeto de tipo PruebaReferencia
PruebaReferencia pr1; // por el momento pr1 vale null
// Asignación de un objeto y almacenamiento de su dirección
// en la variable pr1
pr1 = new PruebaReferencia();

// Creación de una variable por referencia llamada pr2
// en un objeto de tipo PruebaReferencia
PruebaReferencia pr2; // por el momento pr2 vale null
// Copia en pr2 el contenido de pr1, por lo tanto
// la dirección del objeto
pr2 = pr1;

assert( pr1 == pr2 );
assert( pr1.equals(pr2));

La ejecución del programa no provoca la visualización de errores; por lo tanto, los dos métodos generan el
mismo resultado.

Ahora, puede que no desee tener este comportamiento en todos los casos. Tomemos por ejemplo una base de
datos que contiene la información de los clientes. Con una consulta SQL, nuestro programa lee la ficha del
cliente que tiene como identificador único el número 2080. El resultado de la lectura se almacena como un
objeto referenciado Cliente1. En otro lugar del programa, queremos saber si el objeto Cliente1 se ha
modificado desde su carga. Para esto, volvemos a leer desde la base de datos la información del identificador
2080 y creamos un segundo objeto Cliente2 con el resultado de esta lectura.

Si a continuación se utiliza el método java.lang.Object.equals de base, los dos objetos siempre se ven
diferentes porque realmente son dos instancias en memoria y por lo tanto los dos objetos podrían contener los
mismos atributos.

Retomando nuestro método equals, podríamos probar la igualdad «hecha a medida» realizando una
comparación de los atributos.

Observe que el argumento del método equals es una referencia a un tipo Object. Como todas las clases Java
heredan de la clase Object, sus instancias se pueden presentar como instancias de Object (recuerde el
polimorfismo). Habrá que pensar en implementar el método que comprueba que el objeto que pasa como
argumento es el que esperamos. Si es el caso, entonces podremos comparar los atributos que hacen que las
dos instancias sean iguales.

A continuación se muestra el código de una clase Cliente que retoma el método equals. Volveremos más tarde
sobre la sintaxis utilizada.

public class Cliente {



// Volveremos más tarde sobre el rol
// de este método y su sintaxis
public Cliente(int numCliente){
this.numCliente = numCliente;
}

// Atributo importante
int numCliente;
// ... podemos imaginar otros atributos

// El comportamiento del método de base
// se redefine aquí para la clase Cliente
@Override // volveremos más tarde sobre esta definición
public boolean equals(Object obj) {
// obj ¿es de tipo Cliente?
if (obj instanceof Cliente ) {
// Sí. Entonces se crea una referencia temporal
// esta vez "fuertemente tipada Cliente"
// Para esto hacemos un cast de obj en Cliente
Cliente c = (Cliente)obj;
// Se hace más fácil comparar los dos campos.
return numCliente == c.numCliente;
}
// En el resto de los casos, la prueba de igualdad es falsa
return false;
}
}

El operador instanceof permite saber de manera dinámica si un objeto dado es de un determinado tipo.

El operador de cast (...) permite considerar un objeto como de un determinado tipo. Si el tipo no es
compatible, entonces se producirá una excepción ClassCastException (estudiaremos las excepciones más
adelante).

A continuación se muestra un extracto de código que va a comprobar el funcionamiento de nuestra clase


Cliente.

// Creación de tres clientes


Cliente c1 = new Cliente(100);
Cliente c2 = new Cliente(200);
Cliente c3 = new Cliente(100);
// Creación de un Object cualquiera
// para comprobar el funcionamiento de instanceof
Object o = new Object();

// Los numClientes de c1 y c2 son diferentes
assert(c1.equals(c2) == false);
// Los numClientes de c1 y c3 son idénticos
assert(c1.equals(c3) == true);
// Por el contrario, las instancias son diferentes
assert(!(c1==c3)); // la ! invierte el resultado
// de la prueba
assert(c1!=c3); // notación más sencilla
// El objeto comparado no es de tipo Cliente
assert(c1.equals(o) == false);

El objetivo de este pequeño ejemplo es demostrar el principio básico de la comparación. Aquí se ha reducido a
la sencilla prueba del identificador, pero se podría extender a otros atributos e implementar operaciones de
comparación más sofisticadas. Por ejemplo, en el marco de la comparación entre cadenas de caracteres, sería
pertinente no tener en cuenta la diferencia entre las mayúsculas y las minúsculas.

2. hashCode

public int hashCode();

En POO, es importante que cada instancia pueda tener una sola referencia. El método hashCode se puede
utilizar para ello y, de esta manera, llamarse durante la construcción de la lista de referencias.



Cliente c1 = new Cliente(100);
Cliente c2 = new Cliente(100);

int hashCodeDeC1 = c1.hashCode();
int hashCodeDeC2 = c2.hashCode();

assert(hashCodeDeC1 != hashCodeDeC2);
// hashCodeDeC1 y hashCodeDeC2 son totalmente diferentes
// (ex 873697925 y 1895330936)
// por defecto el método hashCode devuelve
// la referencia en memoria de su objeto

Es libre de redefinir el comportamiento de este método en sus clases.

public class Cliente {



// Volveremos más tarde sobre el rol
// de este método y su sintaxis
public Cliente(int numCliente){
this.numCliente = numCliente;
}

// Atributo importante
int numCliente;
// ... podemos imaginar otros atributos

// El comportamiento del método de base
// se redefine aquí para la clase Cliente
@Override // volveremos más tarde sobre esta definición
public boolean equals(Object obj) {
// obj ¿es de tipo Cliente?
if (obj instanceof Cliente ) {
// Sí. Entonces se crea una referencia temporal
// esta vez "fuertemente tipada Cliente"
// Para esto se hace un cast obj en Cliente
Cliente c = (Cliente)obj;
// Se hace más fácil comparar los dos campos.
return numCliente == c.numCliente;
}
// En el resto de los casos, la prueba de igualdad es falsa
return false;
}

@Override
public int hashCode() {
// En la instancia Cliente es el atributo numCliente
// el que especifica el Cliente de manera única
return numCliente;
}
}

Si redefine el método equals() en sus clases, también hay que redefinir el método hashCode(). En efecto,
por defecto, cuando dos objetos son iguales, entonces sus códigos hash también lo son (funcionamiento a
nivel de la superclase Object). A partir del momento en que las reglas de igualdad se redefinen en una clase
heredada, también hay que redefinir el cálculo del código hash, como se ha hecho en el ejemplo anterior.

3. toString
public string toString();

Muy útil durante las fases de puesta a punto, el método toString permite devolver, en forma de cadena de
caracteres, información resumida sobre la instancia de la clase. Colocará en esta información lo que crea que
es más importante para trazar un funcionamiento incorrecto.

En el ejemplo de código siguiente, toString devuelve el contenido del campo numCliente.

public class Cliente {



// Volveremos más tarde sobre el rol
// de este método y su sintaxis
public Cliente(int numCliente){
this.numCliente = numCliente;
}

// Atributo importante
int numCliente;
// ... podemos imaginar otros atributos

// El comportamiento del método de base
// se redefine aquí para la clase Cliente
@Override // volveremos más tarde sobre esta definición
public boolean equals(Object obj) {
// obj ¿es de tipo Cliente?
if (obj instanceof Cliente ) {
// Sí. Entonces se crea una referencia temporal
// esta vez "fuertemente tipada Cliente"
// Para esto se hace un cast obj en Cliente
Cliente c = (Cliente)obj;
// Se hace más fácil comparar los dos campos.
return numCliente == c.numCliente;
}
// En el resto de los casos, la prueba de igualdad es falsa
return false;
}

@Override
public int hashCode() {
// En la instancia Cliente es el atributo numCliente
// el que especifica el Cliente de manera única
return numCliente;
}

@Override
public String toString() {
return "Inst.Cliente con numCliente = " + numCliente;
}

}
La explotación del resultado de toString depende de la estrategia elegida para la puesta a punto de su clase. Es
posible verlo en la ventana Output de IntelliJ IDEA o incluso guardarlo en un archivo de trazas. Sin embargo,
el depurador de IntelliJ IDEA hace una utilización inmediata y muy práctica, que permite ejecutar el programa
línea a línea cuando desea trazar el funcionamiento. Cuando, en este modo, sitúa el cursor del ratón en un
objeto, el depurador muestra una miniventana que contiene el resultado de la llamada al método toString del
objeto subyacente. Cuando, en este modo, se pasa la inicialización del objeto, el depurador muestra el
resultado de la llamada al método toString del objeto nuevamente asignado.

4. finalize
El método finalize está relacionado con el proceso de destrucción de los objetos. El capítulo Creación de clases
contiene una sección que presenta el mecanismo de los destructores en Java. Por el momento quedémonos con
que este método se llama por el garbage collector cuando un objeto queda inaccesible -por lo tanto, cuando no
existen más referencias a él en el programa que lo ha creado- y su «finalización» no se ha realizado todavía.

En Java, el método finalize aparece de la siguiente forma:

protected void finalize()


{
// Código de ’limpieza’
}

En Java, los finalize de las clases heredadas deben llamar explícitamente al finalize de sus clases de base.

Volveremos sobre el proceso de destrucción de los objetos un poco más tarde.

5. getClass, .class y el operador instanceof


El polimorfismo es un medio potente que permite considerar los objetos que pertenecen a una misma familia
de manera uniforme. Dicho esto, puede ser útil conocer el tipo «nativo» de una instancia de subclase.

En el ejemplo que sigue, una clase MantenimientoVehiculos ofrece un método Mantenimiento que recibe como
argumento un objeto de tipo Vehiculo. Por lo tanto, va a poder utilizarlo con cualquier clase que herede de la
clase Vehiculo, como VehiculoAMotor y VehiculoAPedales e incluso Moto. En contraposición, antes o después el
método necesitará saber el tipo real para seleccionar la operación de mantenimiento adaptada.

La clase Object nos ofrece el método getClass, que se utiliza en un objeto (por tanto, en una instancia en
memoria) y la propiedad .class, que se utiliza directamente en un nombre de clase. Es posible identificar de
manera precisa un objeto utilizando las dos herramientas, como se muestra a continuación:


if( miObjeto.getClass() == VehiculoAPedales.class ) {
// La instancia miObjeto es una instancia
// de la clase VehiculoAPedales

Como ya hemos visto, el operador instanceof también es muy práctico porque permite saber si un objeto es
una instancia de una clase particular. Si, como en el extracto de código que sigue, la clase Moto extiende la
clase VehiculoAMotor, que a su vez extiende la clase Vehiculo, entonces un objeto Moto será "instanceof" de
VehiculoAMotor, pero también "instanceof" de Vehiculo.

Volveremos a la sintaxis de la herencia más adelante. Sepa por el momento que para esta declaración se utiliza
la palabra clave extends.

public class Vehiculo {



}

public class VehiculoAMotor extends Vehiculo {



}

public class Moto extends VehiculoAMotor {



}

public class VehiculoAPedales extends Vehiculo {



}

public class MantenimientoVehiculos {



public void Mantenimiento(Vehiculo v) {

// El objeto v ¿va a ser creado como una moto?
if( v.getClass() == Moto.class ) {

// Sí... por lo tanto, no se ha creado
// como VehiculoAMotor
assert(v.getClass() != VehiculoAMotor.class);

// pero sigue siendo
// una instancia de VehiculoAMotor
assert( v instanceof VehiculoAMotor );

// como una instancia de Vehiculo
assert( v instanceof Vehiculo );

// y evidentemente una instancia de Moto
assert( v instanceof Moto );
}
}
}

6. clone
Aquí se trata de crear una instancia «destino» del mismo tipo que el objeto fuente, de volver a copiar el
contenido del objeto fuente, campo a campo, y no de una sencilla duplicación de su referencia.

Al final de la operación de clonado:

Objeto destino != Objeto fuente


Es normal porque realmente habrá dos objetos en memoria y por lo tanto, obligatoriamente dos
referencias diferentes. Es muy aconsejable no tener una asociación entre las dos instancias después del
clonado.

Tipo Objeto destino == Tipo Objeto origen


El objeto creado será del mismo tipo que el objeto fuente. Si clona una instancia de objeto de tipo
Cliente, esperará tener una segunda instancia del objeto de tipo Cliente, que contendrá los valores de la
primera.

Objeto destino.equals(Objeto origen) será verdadero


Condición muy aconsejable, pero no siempre realizable. Los campos de los dos objetos deberían tener
valores idénticos. Si los campos de la clase son de tipos primitivos (o de tipos por referencia
inmutables, que veremos pronto), será posible clonarlos sin problema y sus valores serán realmente
idénticos. Si por el contrario la clase que se va a clonar contiene referencias hacia otras clases, esto se
vuelve mucho más complicado. Tenemos en cuenta que no es necesaria ninguna asociación más entre
las dos instancias después del clonado y es imposible duplicar los objetos referenciados en el original.
Excepto un caso muy particular, los campos por referencia del clon se deberán poner a null.

No todos los objetos son clonables.

Como habrá entendido, este mecanismo no es sencillo y no siempre se puede ofrecer a los usuarios de sus
clases. Es la razón por la que el método Object.clone es de tipo protected y, por lo tanto, no se puede llamar
directamente por el usuario de la clase.

protected Object clone ()

En efecto, Object.clone solo se podrá llamar por un heredado. Este heredado ofrecerá un nuevo método clone,
esta vez de tipo public, que se debe utilizar para obtener su clonado.

La creación del objeto clone y la copia de los campos de tipos primitivos (o las referencias inmutables) se
realizan por Object.clone con la condición de que el heredado declare conocer las consecuencias. Vamos a
adelantar algunos conceptos de los siguientes capítulos, hablando de interfaz. En efecto, para que Object.clone
funcione correctamente, el heredado deberá implementar la interfaz Cloneable. De lo contrario -y de nuevo nos
metemos en temas que se verán más adelante- se producirá una excepción de tipo
CloneNotSupportedException.

El siguiente código muestra este tipo de implementaciones para una clase Cliente.

// Volveremos más tarde sobre la utilización


// y la declaración de las interfaces
class Cliente implements Cloneable {


public Cliente(int numCliente){
this.numCliente = numCliente;
}

// Método de clonado de tipo public
// que devuelve una referencia de tipo Object
@Override
public Object clone(){

// El heredado solicita a su superclase
// clonarlo, es decir, crear
// un objeto del mismo tipo y volver a copiar
// todos los campos primitivos
try { // Volveremos sobre esta sintaxis
Cliente clonDelCliente = (Cliente)super.clone();
return clonDelCliente;
}
// La operación de clonado se desarrolla incorrectamente
// y se produce una excepción
catch (CloneNotSupportedException ex) {
return null;
}
}

// Atributo de tipo primitivo (entero)
int numCliente;
}

Para terminar, a continuación se muestra un extracto de código que solicita un clonado de la clase Cliente:

// Creación de una instancia cliente


// con un número de 123
Cliente _c5 = new Cliente(123);
// Creación de una segunda instancia
// fabricada a partir de la primera
Cliente _c6 = (Cliente)_c5.clone();

// Comprobamos que las dos referencias
// son diferentes
assert(_c5 != _c6);
// Comprobamos que las dos referencias
// tienen el mismo tipo
assert(_c5.getClass() == _c6.getClass());
// Comprobamos que los valores de los campos
// de las dos instancias son idénticas
assert(_c5.equals(_c6) == true);

Observe que el método clone de la clase Cliente devuelve un objeto de tipo Object y que el usuario debe
«transtipar» este retorno como tipo Cliente, es decir, limitar a que el compilador lo considere como un tipo
Cliente.

Por lo tanto, el método protegido Object.clone fabrica un segundo objeto sobre el modelo del primero.

Para los miembros de tipo primitivo de la clase que se ha de clonar, el método duplica los contenidos realizando
una copia muy precisa (bit a bit). El miembro clonado tiene el mismo valor que el miembro original y, a
continuación, los dos miembros no tienen ninguna relación. Cualquier modificación de un miembro de tipo
primitivo del lado del clon no tiene ninguna incidencia sobre el original, como muestra el siguiente extracto de
código:

// Aquí el Cliente _c6 es un clon del cliente _c5


// Por lo tanto, tiene el mismo número de cliente
assert( _c6.numCliente == 123);
// Cargamos este número cliente en el clon
_c6.numCliente = 456;
// Comprobamos la modificación
assert( _c6.numCliente == 456);
// Comprobamos que no hay incidencias
// en el original
assert( _c5.numCliente == 123);

Para los miembros de tipo por referencia, los contenidos también van a duplicarse, pero finalmente todos harán
referencia a los mismos objetos.

Los objetos referenciados no se clonan en la operación, justificando así el calificativo de clonado


«parcial».

Por defecto toda modificación realizada sobre un miembro de tipo por referencia del objeto clonado tendrá una
incidencia sobre el original.

Para ilustrar este principio, retomemos nuestra clase Cliente y añadamos un miembro por referencia sobre una
clase de tipo Empresa.

class Empresa {
public String nombre;
// ... continuación del código
}

class Cliente implements Cloneable {
// Atributo de tipo por referencia
public Empresa empresa = new Empresa();

public Cliente(int numCliente){
this.numCliente = numCliente;
}

// Método de clonado de tipo public
// que devuelve una referencia de tipo Object
@Override
public Object clone(){

// El heredado solicita a su superclase
// clonar, es decir, crear
// un objeto del mismo tipo y volver a copiar
// todos los campos primitivos
try { // Volveremos sobre esta sintaxis
Cliente clonDelCliente = (Cliente)super.clone();
return clonDelCliente;
}
// La operación de clonado se ha desarrollado
// incorrectamente y se produce una excepción
catch (CloneNotSupportedException ex) {
return null;
}
}

// Atributo de tipo primitivo (entero)
int numCliente;
}

Ahora realicemos una modificación a partir del miembro empresa de la instancia clonada para comprobar su
repercusión en la instancia original:

// Creación de un cliente número 123


// que hace referencia a una empresa llamada Original
Cliente _c7 = new Cliente(123);
_c7.empresa.nombre = "Original";

// Creación de un segundo cliente
// obtenido a partir del primero
Cliente _c8 = (Cliente)_c7.clone();

// Comprobamos que el atributo de tipo por referencia
// apunta al mismo objeto
assert( _c7.empresa == _c8.empresa );
assert( _c8.empresa.nombre.equals("Original") );

// Modificamos el objeto empresa del clon
_c8.empresa.nombre = "Clon";
// como es el mismo que el original
// la modificación se repercute.
assert( _c7.empresa.nombre.equals("Clon") );

7. notify, notifyAll y wait


Estos tres métodos se presentarán en el capítulo El multithreading de este libro porque se utilizan mucho para
sincronizar los threads.
La clase java.lang.String
Existe una clase muy «integrada» en la gramática Java: la class String (que forma parte del paquete java.lang).

Esta clase encapsula una colección de caracteres Unicode, encapsulados a su vez por el tipo java.lang.Character.
String es de tipo por referencia (por lo tanto, asignado en el heap), pero por razones de comodidad el uso del
operador new para instanciarlo no es el método más utilizado. En efecto, es suficiente con asignar una cadena
durante la declaración de un objeto String para instanciarlo.

String s = "Viva la programación JAVA:)";

La cadena literal se puede construir.

String hello = "Hola, estamos en el "


+ Calendar.getInstance().get(Calendar.DAY_OF_YEAR)
+ "° día del año";

System.out.print(hello);

A continuación se muestra la salida por la consola correspondiente al 14 de enero:

Hola, estamos en el 14° día del año

Durante la declaración de la cadena literal, el carácter \ (antislash) se toma por defecto como secuencia de
escape.

Por ejemplo, \r\n significa «retorno de carro» y «nueva línea»; por lo tanto, concretamente un salto de línea en
la visualización de la cadena.

En consecuencia, si la cadena literal contiene \ «reales» a visualizar, deben aparecer repetidos.

Ejemplo

string miArchivo1 = "C:\\temp\\miArchivo.txt";

Además de este modo de instanciación tan clásico, la clase String ofrece varios constructores de uso mucho más
específico.

Una instancia de String puede contener 2 GB de caracteres.

El valor de un objeto String es la cadena de caracteres que contiene. Esta cadena no se puede modificar. Incluso
si la clase ofrece métodos que permiten realizar modificaciones en el contenido, hay que entender que la
máquina virtual va a volver a crear una nueva entidad sobre el heap porque el contenido original es inmutable.

El siguiente código demuestra este comportamiento:

class StringVerificador{

public void Prueba() {
String s1 = "Hola";
String s2 = Modificar(s1, "Hello World");
System.out.println("S1=" + s1);
System.out.println("S2=" + s2);

}

String Modificar(String aModificar, String nuevoContenido) {
aModificar = nuevoContenido;
return aModificar;
}

}

La clase StringVerificador ofrece un método Prueba, que instancia un objeto String s1 con el valor «Hola» y a
continuación lo modifica en «Hello World».

Esta modificación se realiza en el método Modificar, que recibe una referencia a s1 como primer argumento y el
nuevo valor como segundo argumento.

El tipo String es de tipo por referencia y cualquier modificación realizada dentro del método de modificación se
debería reportar al objeto String original. De hecho, no lo es. Siendo inmutable el valor inicial, la máquina virtual
crea una nueva cadena y, por lo tanto, una nueva referencia. Si la modificación se hubiera hecho directamente
en el método Prueba, entonces s1 hubiera contenido «Hello World» y el cambio de referencia hubiera sido
transparente. Como la modificación se realiza en un método, se construye una nueva cadena a partir de s1
durante la llamada y esta es la «copia» que la máquina virtual va a modificar, asignándole el nuevo valor. Para
confirmar la manipulación, el método Modificar devuelve el resultado de la modificación que el método Prueba
llamó en el objeto s2. Consecuencia: el original no se ha modificado, como prueba la visualización en la ventana
Output de IntelliJ IDEA.

S1=Hola
S2=Hello World

Si se debe crear una cadena de caracteres dinámicamente, entonces hay que dar prioridad a la utilización
de la clase StringBuilder respecto a la clase String.

Ejemplo de código que se debe evitar

Scanner in = new Scanner(System.in);


String HaIndicado = "Su nombre: ";
System.out.println("Indique su nombre");
HaIndicado += in.nextLine();
HaIndicado += "\r\nSu apellido: ";
System.out.println("Indique su apellido");
HaIndicado += in.nextLine();
HaIndicado += "\r\nSu edad: ";
System.out.println("Indique su edad");
HaIndicado += in.nextLine();

System.out.println(HaIndicado);

Se debe sustituir por

Scanner in = new Scanner(System.in);


StringBuilder HaIndicado
= new StringBuilder("Su nombre: ");
System.out.println("Indique su nombre”);
HaIndicado.append(in.nextLine());
HaIndicado.append("\r\nSu apellido: ");
System.out.println("Indique su apellido");
HaIndicado.append(in.nextLine());
HaIndicado.append("\r\nSu edad: ");
System.out.println("Indique su edad");
HaIndicado.append(in.nextLine());

String resultado = HaIndicado.toString();
System.out.println(resultado);
Ejercicio

1. Enunciado
Cree una aplicación de tipo Consola, que servirá de soporte para las siguientes preguntas.

Cree una variable de tipo int llamada i, cuyo valor sea 10.

Cree una variable de tipo java.lang.Integer llamada j, cuyo valor sea el contenido de i.

Cree una variable de tipo java.lang.Integer llamada k, cuyo valor sea el contenido de i.

Muestre i, j y k.

Añada 1 a i.

Muestre i, j y k.

Verifique con un assert que Integer es de tipo Object.

Muestre los hashcode de j y de k.

2. Corrección
Seleccione el menú File y, a continuación, la opción New Project....

Conserve las opciones por defecto y haga clic en Next.

Marque Create project from template de tipo CommandeLine App.

Llame a su proyecto LabTypesJava, elija un directorio de trabajo que termine por un subdirectorio
llamado LabTypesJava y a continuación, indique com.eni como Base package.

Haga clic en Finish.

El contenido del archivo fuente generado por el asistente es el siguiente:

package com.eni;

public class Main {

public static void main(String[] args) {
// write your code here
}
}

El método main es el punto de entrada del programa y vamos a desarrollar nuestro código dentro. El método
main puede recibir información en línea de comandos. Estos argumentos se copian en una tabla de String
llamada args.

A continuación se muestra el código comentado de este primer ejercicio:

package com.eni;

public class Main {

public static void main(String[] args) {
// Crear una variable de tipo int llamada i
// y poner su valor a 10
int i=10;

// Crear una variable de tipo java.lang.Integer
// llamada j y poner su valor a i
java.lang.Integer j=i;

// Crear una variable de tipo java.lang.Integer
// llamada k cuyo valor sea el contenido de i
java.lang.Integer k=i;

// Mostrar i, j y k
System.out.println(i);
System.out.println(j);
System.out.println(k);

// Añada 1 a i
i++;

// Mostrar i, j y k
System.out.println(i);
System.out.println(j);
System.out.println(k);

// Solo ha cambiado i, lo que prueba que el wrapper
// no es una referencia porque en este caso
// j y k hubiera seguido el cambio.

// Verificar con un assert que Integer
// es de tipo Object
assert(j instanceof Object);

// Mostrar el hashcode de j y de k
System.out.println(j.hashCode());
System.out.println(k.hashCode());

// Los hashcode mostrados son los mismos
// que confirman que los wrappers no son
// referencias
}

}

Compile y a continuación ejecute la aplicación con [Shift][F9] y [Shift][F10].

A continuación se muestra la salida por la consola:


5. CREACIÓN DE CLASES
Introducción
Recordemos que una clase es un modelo que el sistema utiliza para instanciar el objeto correspondiente en
memoria. Son estos modelos los que el desarrollador declara en lo que comúnmente se llama archivos fuentes
(archivos con extensión .java) de su proyecto. Esto es lo que acabamos de hacer en el ejercicio LabTypesJava
con la clase Main.
Paquetes
Las clases Java se agrupan por finalidades en conjuntos llamados paquetes, que a su vez se organizan de forma
jerárquica. Cuando escribe el código fuente y desea utilizar una clase de un paquete dado, debe indicar su «ruta
completa» en cada llamada o, de manera más concisa, declarar su importación en el encabezado del archivo
fuente. Las clases «estándares» del paquete java.lang, no siguen esta regla; son directamente accesibles.

Las clases que va a desarrollar deberán pertenecer obligatoriamente a paquetes que, por lo tanto, tendrá que
crear. La organización de estos conjuntos de clases y de sus jerarquías le permite controlar el «ámbito» de los
paquetes, las clases y los métodos.

Hay convenciones de nomenclatura para los paquetes (como también existen para las clases, los métodos, etc.).
Estas pueden variar de una empresa a otra. Generalmente, el paquete empieza por el nombre de un tipo de
dominio (com, edu, gov, mil, net, org), seguido por un punto (.), seguido por el nombre de la empresa, seguido
por un punto (.). A continuación, puede tener un nombre de proyecto o de un módulo utilizable en varios
proyectos, etc.

Ejemplo de nombre de paquete

com.eni.facturacion.exportConta

Elija nombres de paquetes que resuman la función de la familia de clases que agrupan.

Generalmente, el análisis UML llevado a cabo anteriormente le ayudará a la hora de seleccionar los nombres.
Este nombre debe empezar por una letra o por un guion bajo (_). A continuación puede contener letras
(minúsculas por convención), cifras y guiones bajos. Evite absolutamente utilizar los caracteres acentuados.

Por supuesto, es posible modificar el nombre del paquete, incluso el nombre de la clase principal «con
posterioridad».

Seleccione la ventana Project y el paquete que se va a renombrar.

En este ejemplo, queremos cambiar el nombre del package com.eni por com.miempresa. Para esto, se va a
utilizar una función potente de «refactoring» de IntelliJ IDEA, que va a renombrar el paquete y reportar esta
modificación a todas las clases que la alojan.

Acceda al menú contextual del paquete mediante un clic con el botón derecho del ratón en su entrada.
Seleccione Refactor - Rename... y a continuación indique el nuevo nombre del paquete.

Haga clic en el botón Refactor y compruebe el resultado:

La palabra clave package

Sintaxis de declaración

Package <nombreDelPaquete>;

La declaración del paquete se realiza con la palabra clave package, seguida por el nombre que quiera darle,
terminado por un punto y coma. El código adjunto a este paquete (declaración de las importaciones de otros
paquetes y definición de una clase) llega a continuación.

Naturalmente, un mismo paquete se puede definir en varios archivos fuente. Por el contrario -y al contrario
de lo que sucede en C#- una clase de tipo public solo se puede definir en un único archivo fuente.

La directiva import

En sus paquetes, utilice los tipos que se encuentran en otros paquetes. Por ejemplo, puede utilizar el tipo File y
beneficiarse de sus servicios para la gestión de sus archivos. El tipo File está en el paquete java.io. Puede
utilizarlo declarando su ruta completa:

package com.miempresa;

public class MiPrimeraClase {

public static void main(String[] args) {
java.io.File f = new java.io.File("c:\\hiberfil.sys");
System.out.println(f.exists());
}

}

Si utiliza varias veces los servicios de la clase File y no quiere repetir java.io.File en cada llamada, puede definir
la directiva import java.io.*; en el encabezado del archivo fuente y después de la declaración del paquete.

Package com.miempresa;
import java.io.*;

public class MiPrimeraClase {

public static void main(String[] args) {
File f = new File("c:\\hiberfil.sys");
System.out.println(f.exists());
}

}

Al final, el encabezado clásico de un archivo de código Java contiene una serie de líneas que empiezan por la
directiva import. Estas líneas informan al compilador la lista de los paquetes que podrá recorrer para ir a buscar
las clases utilizadas en este archivo fuente.

Una directiva import solo es válida si el ensamblado que contiene el paquete asociado ha sido referenciado en el
proyecto. Recordemos que un ensamblado puede contener los tipos definidos de varios espacios de nombres.

Si conoce el nombre de la clase que va a utilizar, IntelliJ IDEA le puede ayudar durante la escritura de la directiva
de importación.

Por ejemplo, si el tipo Random se ha utilizado sin declaración, entonces:

los módulos impactados por el error se subrayan con líneas onduladas en rojo,

una pequeña ventana que se abre por encima ofrece la solución que el IDE considera más apropiada.

No vamos a seguir inmediatamente este consejo, sino que vamos a revisar todo aquello que se propone.

Haga clic en Random para que aparezca una pequeña bombilla roja a principio de línea.

Haga clic en la bombilla para visualizar el menú contextual de las soluciones.

La primera opción propuesta consiste en importar una clase Java existente, llamada Random.

Las dos opciones siguientes ofrecen no importar la clase Java existente Random, sino crear una desde
cero.

Seleccione la primera opción y compruebe la adición de la directiva de importación en el encabezado del


archivo, que resuelve todos los errores de la misma manera.

Es muy frecuente tener encabezados que contienen directivas import que son inútiles, como consecuencia de las
modificaciones del código. En este caso, IntelliJ IDEA los muestra en gris.

Puede borrarlos manualmente o pedir al IDE hacerlo por usted para situar el cursor en una de las líneas grises.
Esto va a mostrar una bombilla, esta vez en amarillo como signo de advertencia, porque coleccionar import
inútiles no es un error.

Haciendo clic en la bombilla, abre un menú contextual que contiene una opción de optimización de las
importaciones, que hará desaparecer las líneas inútiles.
Declaración de una clase
Una vez que el paquete se ha declarado, la clase se puede definir después de eventuales líneas de importación.

No es posible declarar más de una clase en el mismo archivo fuente, salvo si se «anidan» o si las clases
son «privadas» del archivo fuente que las contiene.

Si intenta declarar dos clases de tipo public en un mismo archivo fuente, IntelliJ IDEA reacciona como sigue:

Una clase se declara con la palabra clave class, seguida por el nombre que ha elegido. Como para el paquete,
este nombre debe empezar por una letra o por un guión bajo (_). A continuación puede contener letras, cifras y
guiones bajos. Evite utilizar caracteres acentuados y opte por el formato CamelCase. Según las convenciones de
nomenclatura más extendidas, es una buena práctica empezar el nombre de la clase por una letra mayúscula.
Por ejemplo, si escribe una clase que emula un reproductor digital, el nombre en formato «CamelCase» será
ReproductorDigital. Las primeras letras de las palabras relacionadas están en mayúsculas.

La declaración de una herencia eventual interviene a continuación. Se explicará en detalle más adelante. En
efecto, si la clase «extiende» una clase existente, entonces la palabra clave extends precede al nombre de la
superclase.

Si la clase «implementa» una o varias interfaces, entonces la palabra clave implements precede a la lista de las
interfaces soportadas por la clase.

Los miembros de la clase (los atributos y los métodos) se definen a continuación entre paréntesis.

Sintaxis de declaración

visibilidad class NombreClase [[extends ClaseMadre]


[implements Lista interfaces de base]]
{
// cuerpo de la clase
}

Ejemplo

package com.miempresa;

public class ClaseQueHereda extends SuClaseMadre implements
Interface1, Interface2 {
// Cuerpo de la clase
}

El atributo de visibilidad de una clase definida en un paquete puede ser de tipo:

public: la clase será utilizable por todos.

<ninguna definición>: la clase será accesible por las clases del paquete en el que se encuentra.

El resto de los atributos (private, protected) solo tienen sentido para las clases anidadas. Una clase anidada (o
nested class) es una clase definida en otra.

Precedida por el atributo private, la clase anidada solo se podrá utilizar en su clase «contenedora».

Precedida por el atributo protected, la clase anidada solo se podrá utilizar por las clases heredadas de su clase
host.

Las clases anidadas se presentan más adelante.

1. Accesibilidad de los miembros


El nivel de accesibilidad de los miembros de una clase (atributos y métodos) se define por un «modificador de
acceso» que precede la declaración de cada miembro.

Este modificador de acceso puede ser:


public para un acceso sin restricción.

protected para un acceso limitado a la clase, sus heredados y a todos los objetos del paquete que
alberga.

<ninguna definición> para un acceso limitado a la clase y a todos los objetos del paquete que lo
alberga.

private para un acceso limitado a un tipo de la clase.

2. Atributos
Los atributos (en otros términos, las variables) de la clase se deben declarar en el interior de los paréntesis de
la clase.

En tanto que lenguaje objeto, en Java, al igual que en C#, es imposible encontrar una variable definida fuera
de una declaración de clase. Es una de las diferencias significativas con C++, que, para ser compatible con C,
ha tenido que continuar soportando una zona de definiciones «globales».

El respeto a la encapsulación de la POO debe conducir al desarrollador a limitar el nivel de accesibilidad de los
atributos de sus clases.

La buena práctica consiste en poner todos los atributos de sus clases en acceso de tipo privado y, a
continuación, decidir caso por caso su visibilidad y su método de acceso.

Los ejemplos de este libro no respetan escrupulosamente esta regla, para evitar sobrecargar los
archivos fuentes y mostrar lo esencial.

Un atributo se define por su visibilidad, su tipo (entero, cadena, referencia a otro objeto, etc.) y por un
nombre. Si es preciso, el atributo se puede asignar directamente en su definición (diferencia agradable
respecto a C++). El carácter ’;’ termina la definición.

El nombre del atributo sigue las mismas restricciones que las del nombre de la clase o del paquete. La notación
CamelCase siempre es acertada, pero la primera letra del atributo generalmente está en minúscula.

Sintaxis de declaración

visibilidad tipo nombreAtributo [=valor o referencia ];

Ejemplo

package com.miempresa;

public class MiPrimeraClase {

protected int porcentajeDescuento = 20;
private String nombreProducto;
}

Generalmente, no se modificará el valor de un atributo durante todo el ciclo de vida de la clase que lo contiene.
Sin embargo, como vamos a descubrir en la siguiente sección, es posible «congelar» el contenido con la
palabra clave final.

Atributos constantes

Es muy frecuente que un programa necesite datos constantes introducidos durante la declaración de la clase.
Por ejemplo, el nombre que asigna a su programa se podrá utilizar en varios lugares y esto en modo solo
lectura. Sería una pena tener que «duplicar» este nombre cada vez que se utiliza porque el día que tenga que
cambiarlo será necesario hacer la modificación por todos lados.

C y C++ ofrecen una sintaxis basada en la utilización de la palabra clave #define.

Ejemplo

#define TASA_DESCUENTO 20

El compilador sustituye a continuación todas las ocurrencias de TASA_ DESCUENTO por 20.

El problema de esta solución es que TASA_DESCUENTO es débilmente tipado. Sospechamos que se trata de un
tipo numérico, pero ¿se trata de un entero con signo, un entero sin signo, un long, un float o incluso un
double?

Con Java, el problema se resuelve porque se debe definir el tipo del dato constante, que se prefijará con la
palabra clave final para prohibir cualquier modificación posterior.

Sintaxis de declaración

visibilidad final tipo nombreAtributo =valor;


Ejemplo

package com.miempresa;

class MiPrimeraClase
{
private final String nombreProducto = "Aplicación Java";
}

Cualquier intento de modificación de un atributo constante fuera del constructor de la clase se salda
con un error de compilación.

Existe incluso otro medio de definición de valores con el atributo constant. Una variable con un atributo
constant recibe su valor de manera estática durante la escritura de la clase o de manera dinámica con el
constructor de la clase que lo alberga. A continuación, no se puede modificar más.

3. Descriptores de acceso
Para esto, hay que cubrir nuestros atributos con una «capa» protectora. En modo escritura, va a comprobar
que los valores recibidos son compatibles con los soportados. Aquí se habla de pruebas «de negocio» y no de
pruebas relacionadas con el tipo del atributo, que se comprueban automáticamente por el compilador. Por
ejemplo, si un código cliente contenido en un atributo de tipo entero debe obligatoriamente ser superior a 1000
en nuestra aplicación, entonces se deberá implementar una prueba de negocio antes de asignar realmente el
valor del código cliente. Si no existe esta prueba de negocio y el programa que llama pasa 999 como código de
cliente, entonces el compilador no provocará ningún error porque 999 es un entero. En modo lectura, también
vamos a prohibir el acceso directo al atributo que igualmente se podría convertir en intrusivo. Por lo tanto, se
va a ofrecer un par de métodos como atributos que hay que proteger, que permitan leer para el primero y
escribir para el segundo.

En el mundo Java, a esto se le llama descriptores de acceso, con una especificación «getter» para la
lectura y «setter» e incluso «mutador» para la escritura.

Ejemplo:

package com.miempresa;

public class MiPrimeraClase {

// El valor de este atributo debe ser superior
// o igual a 0 e inferior a 100
private int porcentajeDescuento;

// Este método devuelve el valor actual de la entrega
public int getPorcentajeDescuento() {
return porcentajeDescuento;
}

// Este método permite modificar el valor de la entrega
// si el nuevo valor está dentro del límite autorizado.
// Este "setter" devuelve true si el valor se guarda
// o false si el nuevo valor está fuera del límite
public boolean setPorcentajeDescuento(int porcentajeDescuento) {

boolean bRet = false;
//Verifica si el argumento que se pasa
//está en los límites aceptables
if (porcentajeDescuento >= 0 && porcentajeDescuento < 100)
{
this.porcentajeDescuento = porcentajeDescuento;
bRet = true;
}
return bRet;
}
}

Del lado del usuario de la clase, la redacción del código es un poco larga, pero los nombres de los métodos
tienen el mérito de ser muy claros.

Desde el punto de vista del desarrollador de la clase, la redacción de los descriptores de acceso también es un
poco larga, pero afortunadamente IntelliJ IDEA nos puede ayudar durante la escritura, gracias a su módulo de
inserción de código.

Compruébelo usted mismo:

Declare su clase con un atributo.

Sitúe el cursor del ratón en este atributo y haga clic con el botón derecho del ratón para abrir el menú
contextual. A continuación, seleccione Refactor - Encapsulate Fields....

Entonces IntelliJ IDEA nos ofrece una encapsulación del atributo usando descriptores de acceso, por lo
que puede modificar el nombre y la visibilidad.

Haga clic en el botón Refactor. IntelliJ IDEA genera los dos métodos que permiten encapsular el atributo
porcentajeDescuento. Actualmente, no existe ningún control «de negocio» sobre los valores del atributo;
tiene que desarrollarlo usted.
Atributos en modo solo lectura

La sección anterior ha presentado la palabra clave final, que permite ubicar un atributo en modo solo lectura.
Si la regla de encapsulación se respeta, esto afectará al «comportamiento interno» de la clase.

Para un control externo de los datos, puede ser oportuno tener determinados atributos accesibles en modo solo
lectura. Para esto, es suficiente con no definir el método setxxx o hacerlo private como sigue:

package com.miempresa;

public class MiPrimeraClase {
private int porcentajeDescuento;

public int getPorcentajeDescuento() {
return porcentajeDescuento;
}

private void setPorcentajeDescuento(int porcentajeDescuento) {
this.porcentajeDescuento = porcentajeDescuento;
}
}

En este extracto de código, el usuario de la clase solo podrá leer el atributo porcentajeDescuento gracias al
método getPorcentajeDescuento. Cualquier intento de escritura de la propiedad por un usuario externo a la
clase provocará un error de compilación. En contraposición, el atributo sigue siendo modificable por la clase en
sí misma.

Los atributos de tipo «static»

Los atributos que acabamos de presentar se añaden a la instancia de la clase que los ha definido. Por ejemplo,
si ha creado una clase Cliente, es probable que contenga un atributo identificador, cuyo contenido será
diferente para cada instancia Cliente. Ahora, imaginemos un sistema de numeración sencillo: el primer Cliente
tendrá el identificador 1, el segundo el identificador 2, etc.

Para generar esta progresión, la mayor parte del tiempo necesitará un contador de instancias creadas (la
mayor parte del tiempo, porque, si utiliza los servicios de una base de datos, entonces podrá soportar esta
numeración única). O declara este contador en un objeto de gestión de las instancias de clase Cliente, que se
creará en primer lugar y, a continuación, se llama a cada nuevo cliente; o declara este contador como
«miembro de tipo static del tipo Cliente». En este último caso -y es este caso el que nos interesa- el contador
se añade al tipo Cliente y no a una instancia Cliente. Por lo tanto, se comparte entre todas las instancias.

Sabemos que el constructor de la clase es un método llamado para inicializar los atributos de la clase. En cada
creación de una nueva instancia Cliente, este constructor podrá leer el valor del contador de tipo static,
incrementarlo y a continuación copiar su contenido en su atributo identificador.

A continuación se muestra el código asociado a este tipo de utilización:

package com.miempresa;

public class Cliente {

// contadorClientes es un entero estático:
// por lo tanto, se añade al tipo Cliente
// y es accesible por todas las instancias
// de la clase Cliente. Por defecto se ajusta a 1,
// lo que quiere decir que el primer cliente
// tendrá el identificador 1
private static int contadorClientes = 1;

// identificadorCliente es un entero dinámico:
// por lo tanto, se añade a una instancia de Cliente
private int identificadorCliente;

// Un "getter" público permite a los usuarios
// de la clase leer el atributo identificadorCliente
public int getIdentificadorCliente() {
return identificadorCliente;
}
// Un "setter" private permite al objeto Cliente
// actualizar su atributo identificadorCliente
// durante su instanciación (ver constructor)
private void setIdentificadorCliente(int identificadorCliente) {
this.identificadorCliente = identificadorCliente;
}

// El constructor copia el contenido estático de contadorClientes
// y a continuación lo incrementa (añade 1)
public Cliente() {
// contadorClientes ha sido prefijado por Cliente.
// para recordar que este atributo
// se añade a un tipo y no a una instancia
// (en el caso de una instancia, se utilizará this.
// que veremos un poco más adelante)
setIdentificadorCliente(Cliente.contadorClientes);
Cliente.contadorClientes++;
}
}

Observe que contadorClientes se define a 1 en la clase Cliente. Esta asignación se realiza únicamente durante
la primera instanciación de la clase Cliente. El siguiente capítulo ofrece otro método que permite inicializar los
atributos de tipo static, para que puedan recibir valores evaluados durante la ejecución, y no definidos durante
la compilación.

Observe también que, cuando se accede a un dato static, es deseable prefijar el atributo por su tipo de
pertenencia, apuntado como Cliente.contadorClientes en nuestro ejemplo. Durante la relectura del código, el
desarrollador sabe inmediatamente que la variable es de tipo static. En el caso de un atributo añadido a una
instancia, también se podría utilizar la notación this señalada que estudiaremos pronto.

4. Constructores

a. Etapas de la construcción de un objeto

Cuando un desarrollador quiere crear una instancia de su clase Cliente, utiliza la palabra clave new, seguida
por el tipo Cliente.

package com.miempresa;

public class MiPrograma {

public static void main(String[] args) {

// Comienzo de las operaciones
Cliente c = new Cliente();
// ... las operaciones continúan

}

}

En nuestro ejemplo, las clases MiPrograma y Cliente están en el mismo paquete. Por lo tanto,
MiPrograma puede directamente utilizar el tipo Cliente. En caso contrario, será necesario hacer una
importación del paquete que contiene Cliente o definirlo de manera literal, antes de cada utilización de este
tipo (ejemplo: com.miempresa.Cliente).

¿Qué sucede durante esta ejecución?

Concretamente, la máquina virtual Java:

Solicita al sistema operativo un «fragmento» de memoria del tamaño de un objeto Cliente.

Ejecuta el constructor de cada atributo definido en la clase, lo que provoca una inicialización por
defecto (ejemplo: los valores numéricos están todos a 0 y las referencias se posicionan a nulo (no
asignadas)).

Busca si ha redefinido un «constructor» para esta clase Cliente y, si es el caso, lo ejecuta.

Devuelve al programa que llama una referencia al objeto asignado nuevamente.

La segunda etapa es una buena mejora de C++, que se contenta con reportar el bloque «en bruto» asignado
por el sistema operativo. Si el desarrollador olvida inicializar los atributos de su clase, entonces tendrá
determinadas sorpresas durante la ejecución. Con Java, como con C#, esta fase de inicialización sistemática
hace prácticamente que la implementación de un constructor sea opcional, salvo si, por supuesto, se requiere
una operación específica, como la asignación de un identificador cliente.

Sintaxis de declaración de un constructor


visibilidad NombreDeLaClase([argumentos]) {
// Implementación
}

La sintaxis de un constructor empieza por la definición de su visibilidad, que puede ser:

public para autorizar a todo el mundo a crear este tipo de objetos.

protected para un acceso limitado a la clase, sus heredados y a todos los objetos del paquete que lo
alberga.

<ninguna declaración> para un acceso limitado a todos los objetos del paquete que lo aloja.

private para prohibir la instanciación de este tipo. Desarrollaremos más adelante el interés de esta
configuración, que puede parecer extraña.

Al contrario de lo que sucede con un método «clásico», un constructor no define ningún tipo de retorno; por
lo tanto, el nombre de la clase está inmediatamente después de la definición de su visibilidad, seguido por un
paréntesis abierto y otro cerrado. A continuación, la operación del constructor se codifica entre paréntesis.

class Cliente {
public Cliente (){
//...
}
}

b. Sobrecarga de constructores

Un constructor se puede «sobrecargar», es decir, ofrecerse en varias versiones con argumentos diferentes.

Si un constructor debe recibir argumentos, entonces se declararán entre paréntesis.

class Cliente {
public Cliente () {
//...
}
public Cliente (String nombre, Boolean activo){
//...
}
}

Al contrario de lo que sucede con otros lenguajes orientados a objetos como C++ y C#, un constructor
no puede ofrecer valores por defecto para sus argumentos.

Escribir un constructor sin argumentos no es una obligación. La mayor parte de las veces, los atributos
se pueden inicializar directamente durante su declaración con valores adecuados y Java proporcionará
un constructor por defecto, que contendrá todos los usuarios de su clase.

c. Encadenamiento de constructores

Un constructor puede llamar a otro constructor de la misma clase (y/o un constructor de una clase de base).
De esta manera, es fácil encadenar una serie de operaciones para evitar cualquier redundancia de código.

El siguiente extracto de código ilustra este mecanismo de encadenamiento.

Un primer constructor sin argumentos permite generar el identificador único de un cliente.

Un segundo constructor permite recuperar su nombre y su estado de cliente activo.

Problema: hay que hacer que la llamada del segundo constructor también genere un identificador.

Gracias a la secuencia this(), que debe figurar en primera línea en el contenido del constructor con
argumentos, es posible llamar al constructor sin argumentos que genera el identificador.

package com.miempresa;

public class Cliente {

private static int contadorClientes = 1;

private int identificadorCliente;

public int getIdentificadorCliente() {
return identificadorCliente;
}
private void setIdentificadorCliente(int identificadorCliente) {
this.identificadorCliente = identificadorCliente;
}

public Cliente() {
setIdentificadorCliente(Cliente.contadorClientes);
Cliente.contadorClientes++;
}

String nombre;
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}

Boolean activo;
public Boolean isActivo() {
return activo;
}
public void setActivo(Boolean activo) {
this.activo = activo;
}

// Constructor con argumentos
public Cliente (String nombre, Boolean activo) {
// Como siempre buscamos evitar
// la redundancia de código, se va a encadenar
// con el constructor "sin argumentos"
// que va a asignar el identificador
this();
// Aquí la palabra clave this. se utiliza para
// eliminar la ambigüedad entre la variable "nombre"
// que se pasa como argumento y el atributo "nombre"
// de la clase
this.nombre = nombre;
this.activo = activo;
}

}

Este principio se puede extender a otro encadenamiento que acepte argumentos.

En el siguiente ejemplo, se ofrece un tercer constructor de cuatro argumentos. Su operación de llamada al


segundo constructor tiene dos argumentos y él mismo llama al constructor sin argumentos.

public Cliente (String nombre, String apellido


, String empresa, Boolean activo) {
// Encadenamiento sobre el constructor que tiene
// el nombre y el estado activo
// y que él mismo llama al constructor
// sin argumentos
this(nombre, activo);
this.apellido = apellido;
this.empresa = empresa;
}
private String apellido;
public String getApellido() {
return apellido;
}
public void setApellido(String apellido) {
this.apellido = apellido;
}

private String empresa;
public String getEmpresa() {
return empresa;
}
public void setEmpresa(String empresa) {
this.empresa = empresa;
}

Desde el punto de vista del usuario de la clase Cliente, la instanciación es muy sencilla:

package com.miempresa;

public class MiPrograma {

public static void main(String[] args) {

// Inicio de las operaciones
Cliente c = new Cliente("Antonio", "Lara", "Lara S.L.",
true);
// ...continuación de las operaciones
}
}

Dos constructores no pueden tener los mismos tipos de argumentos porque el compilador no los podría
diferenciar. Ejemplo: el constructor Cliente(String nombre) no puede coexistir con el constructor
Cliente(String empresa) porque esperan exactamente el mismo tipo en sus «prototipos».

d. El inicializador static
Es posible definir una serie de operaciones añadidas a un tipo (y no a una instancia) que se ejecuten durante
la primera instanciación de uno de sus objetos. Concretamente, si retomamos el ejemplo anterior de nuestra
clase Cliente, podríamos querer que el atributo contadorClientes no empiece por 1, sino por el valor que
tuviera durante la última utilización del programa. Para esto podemos usar el inicializador static.

public class Cliente {


static {
// Tratamientos
}
// continuación de la clase Cliente
}

Las operaciones realizadas entre los paréntesis que siguen a la palabra clave static solo podrán inicializar
datos de tipo static de la clase, pero con valores evaluados o calculados durante la ejecución (frente a valores
fijados durante la compilación).

El siguiente extracto de código muestra la inicialización del contador de clientes en 2000, usando el
inicializador static.

package com.miempresa;

public class Cliente {

private static int contadorClientes;
static
{
// Las operaciones aquí se lanzarán
// una única vez durante la vida del programa
// Solo pueden afectar a los atributos
// y métodos de tipo static
contadorClientes = LeeNombreDeClientes();
}

public static int LeeNombreDeClientes()
{
// Imaginemos aquí una lectura
// en la base de datos
return 2000;
}
}

e. El inicializador dinámico

Es posible definir una serie de operaciones ejecutadas durante la instanciación de un objeto. Esta sintaxis
puede sustituir ventajosamente al constructor sin argumentos de nuestra clase Cliente porque siempre se
llamará. Además, si redefinimos un constructor con argumentos, no hay necesidad de encadenamiento hacia
el código del inicializador dinámico.

De hecho, las operaciones del inicializador dinámico están entre dos llaves, más habitualmente justo después
de la llave abierta de la definición de la clase Cliente.

public class Cliente {


{
// Tratamientos
}
// continuación de la clase Cliente
}

Entonces, el código de nuestra clase Cliente se convierte en:

package com.miempresa;

public class Cliente {

private static int contadorClientes;
// Inicializador static **********************
static
{
// Las operaciones aquí se lanzarán
// una única vez durante la vida del programa
// Solo pueden afectar a los atributos
// y métodos de tipo static
contadorClientes = LeeNombreDeClientes();
}

public static int LeeNombreDeClientes()
{
// Imaginemos aquí una lectura
// en la base de datos
return 2000;
}

// Inicializador dinámico **********************
{
// Las operaciones aquí se lanzarán
// durante cada instanciación del objeto
setIdentificadorCliente(Cliente.contadorClientes);
Cliente.contadorClientes++;
}

// Constructor con argumentos
public Cliente (String nombre, Boolean activo) {
this.nombre = nombre;
this.activo = activo;
}

public Cliente (String nombre, String apellido
, String empresa, Boolean activo) {
// Encadenamiento sobre el constructor
// que toma el nombre y el estado activo
this(nombre, activo);
this.apellido = apellido;
this.empresa = empresa;
}

//...
}

f. Los constructores de tipo private

¿Para qué puede servir una clase cuyo constructor es privado?

Cuando el constructor es privado y por lo tanto no accesible por el usuario de la clase, sirve para prohibir su
instanciación directa con un new.

Pero ¿con qué objetivo?

Porque este tipo de clase quiere administrar ella misma su instanciación en la aplicación para, por ejemplo,
poder estar solo una única vez en memoria.

Este método es un patrón de diseño (design pattern), más conocido con el nombre de singleton.

El acceso a esta clase de instanciación tan particular se realiza por medio de un método de tipo static,
generalmente llamado getinstance y que se encarga de instanciar el objeto durante su primera llamada.

Ejemplo de clase de tipo singleton

package com.miempresa;

// La clase "singleton"
public class ClaseParaInstanciaUnica {
// El constructor es privado para prohibir
// un new ClaseParaInstanciaUnica() externa
private ClaseParaInstanciaUnica() {
}
// Atributo privado que contiene una referencia
// a un objeto de tipo... ClaseParaInstanciaUnica
private static ClaseParaInstanciaUnica
instanciaUnica = null;

// Método de tipo public static
// encargado de crear "de una vez por todas"
// una instancia de ClaseParaInstanciaUnica
public static ClaseParaInstanciaUnica getInstance()
{
if (ClaseParaInstanciaUnica.instanciaUnica == null)
{
ClaseParaInstanciaUnica.instanciaUnica
= new ClaseParaInstanciaUnica();
}
return instanciaUnica;
}
// Atributo instanciado de tipo String
public String miCadena="";
public String getMiCadena() {
return miCadena;
}
public void setMiCadena(String miCadena) {
this.miCadena = miCadena;
}

// Método que actúa sobre la propiedad instanciada
public void AccionSobreInstanciaUnica(String aAgregar)
{
this.miCadena += aAgregar;
}

// Método que muestra el contenido de la propiedad instanciada
@Override
public String toString() {
return "ClaseParaInstanciaUnica{" + "miCadena=" +
miCadena + ’}’;
}
}

Ejemplo de utilización de esta clase singleton


package com.miempresa;

public class MiPrograma {

public static void main(String[] args) {


// La primera llamada a ClaseParaInstanciaUnica.getInstance()
// va automáticamente a crear la instancia
ClaseParaInstanciaUnica claseParaInstanciaUnica
= ClaseParaInstanciaUnica.getInstance();
claseParaInstanciaUnica.AccionSobreInstanciaUnica("Hello ");

// Las siguientes llamadas solo devuelven
// la referencia única del objeto ClaseParaInstanciaUnica
ClaseParaInstanciaUnica claseParaInstanciaUnica2
= ClaseParaInstanciaUnica.getInstance();
claseParaInstanciaUnica2.AccionSobreInstanciaUnica("World");

// getInstance()también se puede utilizar
// directamente en una línea de código
System.out.println(
ClaseParaInstanciaUnica.getInstance().toString());

}
}

Salida por la consola correspondiente

ClaseParaInstanciaUnica{miCadena=Hello World}

Si su programa contiene varias clases (que siempre será el caso) y desea escribir un archivo de trazas central
para ponerlo a punto, entonces la clase que gestionará este archivo de trazas podrá ser de tipo singleton.

g. El «builder pattern»

Si la clase contiene un número importante de atributos para inicializar, la lista de constructores


sobrecargados corre el riesgo de ser larga de escribir, provocando finalmente una flexibilidad de utilización
reducida.

Existe otra forma de constructor que se puede invocar en lenguaje Java: el builder pattern.

El builder pattern consiste en utilizar una clase «de fábrica», que va a crear una instancia de la clase «final»,
con los atributos adecuados. El usuario de la clase «de fábrica» podrá pasar los argumentos en el orden que
quiera y, si omite algunos, entonces se podrán utilizar sus valores por defecto (valores de fábrica).

Veamos todo esto con nuestra clase Cliente y su ClienteBuilder asociada:

package demobuilderpattern;

// Una clase Cliente
class Cliente{

// Una lista de atributos privados
private Boolean clienteActivo;
private int numero;
private String nombre;
private String apellido;
private String empresa;

// Un constructor que permite
// rellenar TODOS los atributos
public Cliente( Boolean clienteActivo,
int numero, String nombre,
String apellido, String empresa){
this.clienteActivo = clienteActivo;
this.numero = numero;
this.nombre = nombre;
this.apellido = apellido;
this.empresa = empresa;
}
}

// Una clase "de fábrica" de objetos cliente
class ClienteBuilder {

// Una lista de atributos privados
// de mismos tipos que en Cliente
// con valores por defecto
private Boolean isClienteActivo = true;
private int suNumero = -1;
private String suNombre;
private String suApellido;
private String suEmpresa;


// Un juego de métodos que tiene
// los mismos nombres que los atributos
// de Cliente para más legibilidad
public ClienteBuilder clienteActivo(Boolean isClienteActivo) {
this.isClienteActivo = isClienteActivo;
return this;
}
public ClienteBuilder numero(int suNumero) {
this.suNumero = suNumero;
return this;
}
public ClienteBuilder nombre(String suNombre) {
this.suNombre = suNombre;
return this;
}
public ClienteBuilder apellido(String suApellido) {
this.suApellido = suApellido;
return this;
}
public ClienteBuilder empresa(String suEmpresa) {
this.suEmpresa = suEmpresa;
return this;
}

// Para terminar un método global de construcción
// de un objeto Cliente con todos los argumentos
public Cliente buildCliente() {
return new Cliente( isClienteActivo,
suNumero, suNombre,
suApellido, suEmpresa);
}
}


public class DemoBuilderPattern {

public static void main(String[] args) {
// Utilización del ClienteBuilder
// para crear un cliente
Cliente miCliente
= new ClienteBuilder()
.clienteActivo(Boolean.TRUE)
.numero(456)
.nombre("Andrés")
.apellido("Lara")
.empresa("ENI")
.buildCliente();
}
}

La sintaxis final desde el punto de vista del usuario para crear una instancia de una clase cliente es realmente
sencilla y amigable. Observe que vale cualquier orden de llamada de los métodos de configuración de los
atributos. En contraposición, es necesario que la llamada a buildClient termine la secuencia. Como se ha
indicado antes, observe también que ClientBuilder contiene determinados valores por defecto para los
atributos del cliente. Esto quiere decir que, si el usuario no indica otra cosa, estos serán los valores que se
utilizarán.

El modo de instanciación de objetos por el uso del builder Pattern ofrece al lenguaje Java una funcionalidad
similar a la de los inicializadores de objetos de C# y a los constructores con argumentos por defecto de C++.

5. Destructores
El destructor es el método de la clase que llama la máquina virtual Java, justo antes de la des-asignación del
objeto. También se habla de «finalización» del objeto y el código se aloja en un método opcional llamado
finalize.

En C++, el desarrollador destruye él mismo los objetos que él ha asignado (es decir, asignados por un new)
cuando ya no los necesita y, por lo tanto, controla en qué momento se llamado el destructor. Este principio
funciona perfectamente con la condición de no olvidar llamarlo.

Tanto en Java como en C#, es el garbage collector el que administra este trabajo y por lo tanto, el
desarrollador ya no es responsable de esta tarea, lo que significa que pierde el control del momento de la
destrucción.

Regla de oro: no piense nunca en los destructores para ubicar operaciones que dependan de la lógica de
ejecución de su programa.

Si un código de «limpieza» se debe ejecutar sistemática y rápidamente al final de la utilización de una


instancia, es muy aconsejable exponerlo en un método público comúnmente llamado close o dispose o incluso
cancel.

Si el usuario de la clase es concienzudo y va a llamar a este método close al final de la utilización, entonces la
limpieza de su objeto se ejecutará en el momento correcto.

Sin embargo, debe prever el caso en el que el usuario olvide llamar a su método close. Entonces tiene dos
soluciones:
No hacer nada... Entonces habrá fugas de memoria y el desarrollador entenderá, después de la
investigación, que ha olvidado llamar a su método close.

Implementar un código de limpieza «si puede» en el método finalize. Cuando la máquina virtual Java
haya entendido que el objeto ya no se utiliza más y por lo tanto nadie lo referencia, llamará a su
método finalize antes de la des-asignación definitiva del objeto. Atención, el sencillo hecho de declarar
un método finalize va a ralentizar la destrucción del objeto porque la operación se va a realizar en un
flujo con prioridad débil (más adelante abordaremos la noción de thread y, por lo tanto, de flujo de
ejecución).

Destructores y métodos close son opcionales. Se deben implementar si el objeto ha utilizado recursos de
sistema (como un acceso a la base de datos) o si se ha realizado la actualización de atributos de tipo
static.

Un finalize es único en una clase; es de tipo protected, no recibe ningún argumento y no devuelve nada.

Sintaxis de un finalize

protected void finalize(){


//...
};

Si su clase se hereda de otra clase (por lo tanto, extiende otra clase) y ha implementado un finalize, entonces
debe pensar en «encadenarlo» al finalize de la clase madre llamando a super.finalize(); (volveremos más
adelante sobre la sintaxis de comunicación entre la clase heredada y la clase madre).

Desde el punto de vista del usuario de una clase que debe realizar operaciones específicas al final de la
explotación, la mejor sintaxis es la del try-finally, que estudiaremos en el capítulo dedicado a las excepciones.
En pocas palabras, esta sintaxis es perfecta si la duración del ciclo de vida del objeto en cuestión se limita a un
método. Entonces, el desarrollador instancia el objeto, a continuación en un bloque try pide la ejecución de una
operación de este objeto y después, en el bloque finally, llama a su método de limpieza (close por ejemplo). El
contenido del bloque finally siempre se ejecuta, incluso si la operación dentro del try es incorrecta. Por lo tanto,
es el lugar ideal para llamar a un código de limpieza.

Para ilustrar esta sección, a continuación se muestra el código de una clase que implementa los métodos Close
y finalize.

Package democlosefinalize;

class MiClase {
// Contador static (por lo tanto, común a todas las instancias)
private static int numeroInstancias = 0;

// Su descriptor de acceso
public static int getNombreInstancias() {
return MiClase.numeroInstancias;
}

// El constructor de instancia
public MiClase(){
// Incrementa el contador estático
MiClase.numeroInstancias++;
// Si es la primera instancia
// entonces la lógica de negocio de esta clase
// quiere que se abra un archivo.
if( MiClase.getNombreInstancias() == 1) {
AperturaArchivo();
}
// else
// Si por el contrario no es la primera instancia,
// entonces el archivo ya está abierto
// y por lo tanto todas las instancias (entre ellas esta)
// lo podrá utilizar
}

public void MiTrabajo(){
// Aquí se utiliza la instancia de la clase
// para hacer las operaciones que van a
// utilizar el archivo común a todos
//...
}

// El desarrollador atento llamará
// al método Close() cuando haya terminado
// de utilizar la instancia de la clase
public void Close(){
// Empieza a decrementar el contador de instancia
MiClase.numeroInstancias--;
if( MiClase.getNombreInstancias() == 0 ){
// Si este contador llega a cero
// entonces el archivo se debe cerrar.
CierreArchivo();
}
}

// Si, por el contrario, el desarrollador ha olvidado llamar
// el método Close, entonces de todas formas vamos a
// volver a cerrar el archivo durante el finalize.
// Atención: no sabemos CUÁNDO este método
// se llamará por el garbage collector
@Override
protected void finalize() throws Throwable {

// Si el desarrollador ha olvidado llamar al método
// Close entonces el archivo permanece abierto.
if( MiClase.isArchivoAbierto() ){
CierreArchivo();
}

// Llamada del finalize del padre
// En nuestro caso el de Object...
// que no hace gran cosa.
super.finalize();
}


// Atributo que memoriza la apertura o no del archivo
private static boolean archivoAbierto = false;

// Su descriptor de acceso
public static boolean isArchivoAbierto() {
return MiClase.archivoAbierto;
}

// Inicio de las operaciones: apertura
private void AperturaArchivo() {
//...
MiClase.archivoAbierto = true;
}

// Fin de las operaciones: cierre
private void CierreArchivo() {
//...
MiClase.archivoAbierto = false;
}

}

Utilización correcta de MiClase:

public class DemoCloseFinalize {



public static void main(String[] args) {

// Instanciación de un objeto MiClase
MiClase miClase = new MiClase();
// Aquí miClase ha abierto el archivo

try{
// Explotación miClase
miClase.MiTrabajo();
}
finally{
// Sea cual sea el resultado de MiTrabajo
// se llamará esta porción de código
// y por lo tanto nos beneficiamos de la llamada
// al método de "limpieza"
miClase.Close();
}
}
}

6. La palabra clave this


Ya hemos utilizado el this varias veces y es momento de indicar un poco más su sentido.

En Java (como en C# y C++), se ofrece la palabra clave this para acceder a la instancia de la clase actual.

Atención, tanto en Java como en C#, this se utiliza con un puntero para acceder a los miembros
(this.identificador = 523). En C++ se utiliza con una flecha (this->m_identificador = 523;) porque es un
puntero.

IntelliJ IDEA nos ayuda en la redacción de nuestro código; el sencillo hecho de indicar this en un método de la
clase invoca al asistente que ofrece todos los miembros asociados a la instancia.
La utilización del this normalmente es opcional y algunas veces puede complicar la lectura del código.

public Cliente(String nombre)


{
this.Nombre = nombre;
// equivale a:
Nombre = nombre;
}

A pesar de todo, si una clase contiene miembros de tipo static -por lo tanto, miembros asociados al tipo de la
clase y no a su instancia- entonces la utilización del this puede permitir una mejor comprensión del código.

En el siguiente extracto se ve muy claramente que el miembro asociado a la instancia de la clase Cliente es
identificadorCliente y que el miembro asociado al tipo Cliente es contadorClientes.

public Cliente()
{
this.identificadorCliente = Cliente.contadorClientes;
Cliente.contadorClientes++;
}

La utilización de this. puede evitar ambigüedades cuando los nombres de las variables y de los atributos son
idénticos.

En el siguiente extracto de código, se han seleccionado nombres comunes para los atributos y los argumentos
asociados del constructor. La utilización de this permite evitar la ambigüedad entre argumentos del constructor
y atributos de la clase.

private String direccion;


private String codigoPostal;
private String ciudad;
private String pais;

public Cliente(String nombre, boolean activo, String direccion,
String codigoPostal, String ciudad, String pais)
{
this.direccion = direccion;
this.codePostal = codigoPostal;
this.ciudad = ciudad;
this.pais = pais;
}

Hemos visto que this también permite encadenar constructores del mismo nivel y evitar la duplicación de
código. El siguiente extracto de código muestra este uso para realizar el encadenamiento entre tres
constructores.

public Cliente () {
// Las operaciones aquí se lanzarán
// en cada instanciación de objeto
setIdentificadorCliente(Cliente.contadorClientes);
Cliente.contadorClientes++;
}

// Constructor con argumentos
public Cliente (String nombre, Boolean activo) {
this(); // llama al constructor sin argumentos
this.nombre = nombre;
this.activo = activo;

}

public Cliente (String nombre, String apellido
, String empresa, Boolean activo) {
// Encadenamiento en el constructor
// que tiene el nombre y el estado activo
// y que, él mismo, llama al constructor
// sin argumentos
this(nombre, activo);
this.apellido = apellido;
this.empresa = empresa;
}

Durante el siguiente uso de la clase Cliente:

Cliente c1 =
new Cliente("Antonio", "Lara", "Lara S.L.", true);

El encadenamiento hace que sea el código de Cliente() el que se ejecute en primer lugar, después, el de
Cliente(String nombre, bool activo) y para terminar Cliente(String nombre, String apellido, String empresa,
Boolean activo).

7. Métodos
Ya lo hemos implementado alguna vez y, por lo tanto, adivinamos que los métodos contienen las operaciones y
comportamientos de una clase. Se trata de las porciones de programas ejecutados por el propio objeto de
manera interna o desde otros objetos con los permisos necesarios. Todos los métodos, incluido main, que es el
punto de entrada de la aplicación, están contenidos en las definiciones de clase.

a. Declaración

Sintaxis de un método

[Atributo visibilidad][Modificador]<tipo de retorno><Nombre>([tipo


param], [tipo param2],...)<throws excepción1, excepción2>{
<Código>;
< Código >;
//...
}

Los atributos de visibilidad

Como sucede con el resto de los miembros de la clase, los métodos generalmente están precedidos por un
atributo de visibilidad:

public para permitir a todo el mundo utilizar el método.

protected para un acceso limitado a la clase, sus heredados y a todos los objetos del paquete que
aloja.

private para que el método se utilice únicamente de manera interna por el objeto o por una segunda
instancia de un objeto del mismo tipo.

Si no se ha definido ningún atributo, entonces el método será accesible para todos los objetos del
paquete que lo contienen.

Los modificadores opcionales

Después del atributo de visibilidad, se puede definir un modificador.

El modificador static declara el método como adjunto a un tipo y no a un objeto. Ya hemos abordado esta
noción para los atributos y es el mismo principio para los métodos. Generalmente, el desarrollador reúne en
un juego de métodos de tipo static las operaciones que no justifican la instanciación de un objeto, sino que se
recogen en una clase «temática». En pocas palabras, se van a crear métodos de tipo static tan pronto como
no haya ninguna información para memorizar entre dos llamadas. Por ejemplo, una clase Calcula podrá
ofrecer un juego de métodos de tipo static que recoge los argumentos de sus operaciones como argumentos
y devuelve directamente el resultado.

Si no es necesario ningún atributo para las operaciones y los métodos, se agrupan bajo la declaración de una
clase con el nombre Calcula.

Una clase también puede mezclar métodos «dinámicos» (adjuntos a una instancia) y métodos de tipo static.
Los métodos dinámicos pueden acceder a los miembros de tipo static, pero no a la inversa. Los métodos de
tipo static solo podrán utilizar los miembros de tipo static de la clase.

Java ofrece los modificadores abstract y final para influir en las reglas de herencia. Estos modificadores se
estudiarán en el capítulo dedicado a la herencia.

El tipo de retorno

Después de un eventual modificador o atributo de visibilidad, se define el tipo de retorno del método. Este
retorno puede ser:

La palabra clave void para indicar que el método no devuelve nada.

Un tipo primitivo (un int por ejemplo).

Un tipo que pertenece a la familia de referencia (una referencia a una clase Cliente por ejemplo).

En este último caso, un objeto instanciado en el cuerpo del método puede seguir a la ejecución de este
método (paréntesis cerrado del método) si su referencia se devuelve para ser copiado y explotado por el
código que llama.

El nombre del método

A continuación se declara el nombre del método. Las reglas son las mismas que para los nombres de
atributos, a saber, empiezan por una letra o por un guión bajo (_). A continuación, puede contener letras,
cifras y guiones bajos. Evite utilizar los caracteres acentuados y, si su definición contiene varias palabras,
opte entonces para el formato CamelCase, por ejemplo: MostrarColeccion. De una manera general, siempre
es preferible los nombres de métodos explícitos, que hagan referencia a sus funciones en su clase. Los
nombres «extendidos» no son un problema gracias al asistente para la escritura. Por lo tanto, evite nombres
del tipo Funcion1 o MetodoBis...

Los argumentos del método

Después de que se declare el nombre, los argumentos del método se declaran entre paréntesis. Incluso si el
método no recibe ningún argumento, hay que añadir un paréntesis de apertura y otro de cierre.

boolean ExportaEnContabilidad(){
//...}

Si el método espera argumentos, entonces se definen después del paréntesis de apertura como una lista de
parejas tipo/nombre, separados por comillas.

Durante la utilización del método, el código que llama pasará los argumentos. El tipo de los argumentos se
deberá corresponder con los tipos de los argumentos esperados en el método. Los nombres, por el contrario,
podrán ser diferentes, como se puede comprobar en el siguiente extracto de código, con un double radio
como argumento y un double r como argumento.

package com.miempresa;

class Calcula {
public double Perimetro(double r)
{
return 3.14 * r * 2;
}
}

public class MiPrograma {

public static void main(String[] args) {

Calcula c = new Calcula();
double radio = 2.3;
// La variable radio se pasa como argumento
// aunque el método espere una variable
// llamada r... Es el tipo de la variable
// que se pasa y no su nombre lo que cuenta para
// no provocar errores en el compilador.
double perimetro = c.Perimetro(radio);
System.out.println("perimetro=" + perimetro);
}
}

Eventualmente se podrá producir una la lista de excepciones

Vamos a estudiar un poco más adelante el principio de las excepciones. En resumen, cuando se utiliza en la
definición del método la clausula throws con una lista de excepciones, esto quiere decir que el usuario del
método debe esperar a tener ejecuciones que se detengan repentinamente, como consecuencia de
funcionamientos incorrectos «excepcionales». Por supuesto, aquí nos encontramos más allá de un código de
error devuelto por la función; realmente se trata de un salto en la ejecución, desde el método que provoca el
problema a la búsqueda de una operación adecuada. Estudiaremos esto en la sección relacionada con la
gestión de las excepciones.

Las instrucciones del método


Después del paréntesis de cierre en la definición de los argumentos, empieza la implementación del código.
Esta vez, es una llave de apertura la que precede a la operación y una llave de cierre el que la termina. Cada
línea de la operación se debe terminar por un punto y coma. La instrucción return con o sin argumentos de
retorno permite finalizar la ejecución del método en cualquier sitio. En C/C++ es muy aconsejable tener solo
un único punto de retorno por método, evitando olvidar liberar los bloques de memoria creados. En Java y en
C#, gracias al garbage collector, el problema ya no se presenta.

b. Paso de argumentos por valor

En determinados lenguajes, existen dos modos para pasar argumentos a un método: el modo de paso por
valor (el más frecuente) y el modo de paso por referencia. El primero hace una copia del argumento y, por lo
tanto, cualquier modificación dentro del método no tiene incidencia sobre el dato original. El segundo pasa la
dirección del argumento y, en este caso, el método interviene directamente sobre el original. Normalmente
hay confusión entre estos dos modos de paso de argumentos.

Con el lenguaje Java, las cosas son más sencillas porque los argumentos siempre se pasan por valor.

Hay que entender bien las diferencias que puede haber con los tipos de los argumentos que se pasan.

Recordemos que en Java hay dos tipos de datos:

los tipos primitivos (enteros, booleanos, caracteres, etc.),

los tipos por referencia (instancias de clase).

Cuando un método recibe como argumento un tipo primitivo, se realiza una copia en una variable local al
método.

El siguiente extracto de código muestra y comenta el paso de un argumento entero por valor.

package com.miempresa;

class Demo {

public void Execute(){

PruebaConValores t = new PruebaConValores();
int contador = 10;
System.out.println("contador antes de llamada: "
+ contador);
// Durante la llamada a PasoValorPorValor
// se realiza una copia de contador
t.PasoValorPorValor(contador);
// Aquí contador vale siempre 10
// y la copia ha desaparecido
System.out.println("contador después de la llamada: "
+ contador);
}
}

class PruebaConValores {
// La variable i es local al método
// Es la copia de contador
public void PasoValorPorValor(int i) {
// En la entrada del método i vale 10
System.out.println("i en la entrada del método: "
+ i);
// Se puede modificar sin incidencia
// sobre contador
i = 0;
System.out.println("i en la salida del método: "
+ i);
} // i va a desaparecer aquí... snif
}

public class MiPrograma {

public static void main(String[] args) {

// El Main instancia un objeto de tipo Demo
// y llama a su método Execute
Demo d = new Demo();
d.Execute();
}
}

Salida por la consola:

contador antes de la llamada: 10


i en la entrada del método: 10
i en la salida del método: 0
contador después de la llamada: 10

La copia de un tipo por valor genera otro valor, es decir, una nueva entrada en la memoria de tipo pila
(stack). Las modificaciones añadidas a la copia no tienen consecuencia sobre el valor inicial.
Para modificar el original, una primera solución consistiría en devolver la copia desde el método y, a
continuación, sustituir el valor original en el cuerpo principal, como en el siguiente código.

package com.miempresa;

class Demo {

public void Execute(){

PruebaConValores t = new PruebaConValores();
int contador = 10;
System.out.println("contador antes de la llamada: "
+ contador);
// Durante la llamada a PasoValorPorValor
// se realiza una copia de contador
// En el retorno del método, es modificado
// el contenido de contador
contador = t.PasoValorPorValor(contador);
// Por lo tanto, aquí contador vale ahora 0
// y la copia ha desaparecido
System.out.println("contador después de la llamada: "
+ contador);
}
}

class PruebaConValores {
// La variable i es local al método
// Es la copia de contador
public int PasoValorPorValor(int i) {
// En la entrada al método i vale 10
System.out.println("i en la entrada del método: "
+ i);
// Se puede modificar sin incidencia
// sobre contador por el momento...
i = 0;
System.out.println("i en la salida del método: "
+ i);
// i modificado se devuelve ahora
return i;
} // i va a desaparecer aquí... snif
}

public class MiPrograma {

public static void main(String[] args) {

// El Main instancia un objeto de tipo Demo
// y llama a su método Execute
Demo d = new Demo();
d.Execute();
}
}

Salida por la consola:

contador antes de la llamada: 10


i en la entrada del método: 10
i en la salida del método: 0
contador después de la llamada: 0

Esta solución es la más utilizada cuando el método solo debe devolver un único argumento. Si, por ejemplo,
desea devolver el resultado de un cálculo y un código de error, esta sintaxis no es conveniente.

La segunda solución consiste en crear una clase para encapsular el tipo primitivo en un objeto. ¿Qué sucede
en este caso concreto? Un objeto de tipo por referencia tiene su propia zona de memoria en la pila, que
contiene sus miembros y una referencia a esta zona registrada en una variable tipada inscrita en la pila
(stack).

Cuando se ejecuta:

MiClase mc = new MiClase();

Tenemos dos partes:

un objeto de tipo MiClase que se encuentra en el heap,

la variable mc, esta vez escrita en el stack, que referencia a la instancia del objeto MiClase en el heap.

El desarrollador C++ rápidamente hará la analogía entre el bloque asignado en memoria y su puntero.

Como hemos visto con anterioridad, un paso por valor implica una copia del argumento. Si el método espera
un objeto de tipo por referencia, el mecanismo de llamada va a duplicar esta referencia y la va a pasar al
método. Pero como esta referencia duplicada apunta al mismo objeto, entonces el método llamado tendrá
acceso al objeto original. El siguiente extracto de código ilustra esta explicación:

package com.miempresa;

class MiClaseDePrueba {
private int miInt;

public int getMiInt() {
return miInt;
}
public void setMiInt(int miInt) {
this.miInt = miInt;
}

public MiClaseDePrueba(int miInt) {
setMiInt(miInt);
}

// Muestra la referencia en el heap
// y el valor de la propiedad miInt
public String ToString()
{
return "Ref:" + this.hashCode()
+ " MiInt:" + this.miInt;
}
}

class PruebaConReferencias {
// La variable m es local al método
// y es una copia de mc (ver más adelante Execute() )
public void PasoRefPorValor(MiClaseDePrueba m) {
// En la entrada al método m.miInt vale 10
System.out.println(
"m en la entrada al método: " + m.ToString());

m.setMiInt(0);
System.out.println(
"m en la salida al método: " + m.ToString());
}
}
class Demo {

public void Execute() {

PruebaConReferencias t = new PruebaConReferencias();

// Instanciación de un objeto MiClaseDePrueba
// con inicialización de su propiedad miInt a 10
MiClaseDePrueba mc = new MiClaseDePrueba(10);
// Visualización en la consola
System.out.println(
"mc antes de la llamada: "+ mc.ToString());

// Durante la llamada a PasoRefPorValor
// se crea una copia de la referencia mc
// en el stack
t.PasoRefPorValor(mc);

// Por lo tanto, aquí mc.miInt vale ahora 0
// porque la copia de mc referenciaba
// al mismo objeto en el heap.
System.out.println(
"mc después de la llamada: " + mc.ToString());
}
}


public class MiPrograma {

public static void main(String[] args) {

// El Main instancia un objeto de tipo Demo
// y llama a su método Execute
Demo d = new Demo();
d.Execute();

}

Salida por la consola:

mc antes de la llamada: Ref:1288110412 MiInt:10


m en la entrada al método: Ref:1288110412 MiInt:10
m en la salida al método: Ref:1288110412 MiInt:0
mc después de la llamada: Ref:1288110412 MiInt:0

Observe que object.hashCode() devuelve la referencia al objeto en memoria heap. Durante todos los
intercambios, esta referencia permanece inalterable (Ref:1288110412), lo que prueba que se accede a la
misma zona de memoria.

En lenguaje Java, al igual que en C, C++ y C#, los argumentos de un método son variables locales al
método obtenido por copia de los datos que se pasan en la llamada.
Cuando un método recibe un argumento de tipo primitivo, entonces trabaja en una copia de la
variable pasada por la llamada.

Cuando un método recibe un argumento de tipo por referencia, entonces trabaja con una copia de la
referencia que se pasa por la llamada. Copia y original referencian al mismo objeto. Por lo tanto, el
método trabaja directamente sobre el objeto de la llamada Y NO SOBRE UNA COPIA.

8. Sobrecarga de métodos
Como sucede con sus constructores, una clase puede ofrecer varios métodos con el mismo nombre, pero con
argumentos diferentes. Esta posibilidad permite adaptar el mismo «verbo» (nombre del método) a diferentes
circunstancias.

El siguiente extracto de código muestra un ejemplo de sobrecarga (overloading) del método Perimetro,
declinado aquí en tres versiones.

package demosobrecargametodos;

class Calcula {
// Método dedicado rectángulo
public double Perimetro(double longitud,
double altura) {
return 2 * longitud + 2 * altura;
}
// Método dedicado triángulo
public double Perimetro(double lado1,
double lado2, double lado3) {
return lado1 + lado2 + lado3;
}
// Método dedicado círculo
public double Perimetro(double radio) {
return 3.14 * radio * 2;
}
}

public class DemoSobrecargaMetodos {
public static void main(String[] args) {

Calcula c = new Calcula();
double perimetro = 0;

// Calcula el perímetro de un círculo
double radio = 2.3;
perimetro = c.Perimetro(radio);
// Calcula el perímetro de un triángulo
double c1 = 2, c2 = 10, c3 = 5;
perimetro = c.Perimetro(c1, c2, c3);
// Calcula el perímetro de un rectángulo
double longitud = 10, altura = 13;
perimetro = c.Perimetro(longitud, altura);

}
}

Es el compilador el que decide, en función de los argumentos que se pasan, el método al que se debe llamar.
Para esto, necesariamente las «firmas» de los métodos deben ser diferentes. En el caso de la sobrecarga, la
firma del método incluye su nombre, sus argumentos, pero no su tipo de retorno.

Por ejemplo, el siguiente extracto de código es incorrecto.

public double Perimetro(double radio) {


return 3.14 * radio * 2;
}

public double Perimetro(double diametro) {
return 3.14 * diametro;
}

El compilador devuelve el error «el método Perimetro (double) ya se ha definido en la clase Calcula»:
No dude en sobrecargar sus métodos para que sus objetos sean más atractivos; su programación será
«intuitiva».

9. Mecanismo de las excepciones

a. Presentación

Las anomalías detectadas durante la ejecución por la máquina virtual de Java se reportan a la aplicación a
través de un mecanismo conocido con el nombre de mecanismo de las excepciones.

Todavía no hemos tratado las tablas, pero sepa que se instancian para contener un número definido de
casillas y, por lo tanto, si ejecuta el siguiente código, la máquina virtual Java va a «provocar» una excepción
porque el programa intenta escribir fuera de los límites de la tabla (tab[7] = 3;).

public class MiPrograma {



public static void main(String[] args) {


int[] tab = new int[5];
tab[7] = 3; // excepción porque el índice va de 0 a 4
}

Dando por hecho que el código no ha previsto tratar las excepciones, la ventana Output contiene un mensaje
que informa al usuario de que se ha producido un problema no gestionado.

El mensaje es muy concreto; indica que la excepción es de tipo java.lang.ArrayIndexOutOfBoundsException y


que se ha provocado como consecuencia de la utilización de un índice fuera de los límites de la tabla.

En este caso se trata de un «bug de codificación» que se ha colado durante las pruebas y que vamos a tratar
pronto.

Hay otras fuentes de error, como los datos erróneos introducidos. Por ejemplo, el programa espera una cifra y
el usuario indica una serie de letras. Si no se ha previsto ninguna operación de comprobación, se producirá
un error durante la conversión.

También hay errores devueltos por el propio «sistema». Por ejemplo, su programa está escribiendo un
archivo en una llave de memoria USB, que el usuario extrae antes de que finalice el trabajo.

El mecanismo de las excepciones se desencadena para estos tres casos de error. Naturalmente, el
desarrollador pueden evitar las dos primeras fuentes de error. Sin embargo, si persisten los errores, entonces
se producirán excepciones.

Las excepciones no están reservadas a la máquina virtual Java. Sus clases también podrán provocar
excepciones y, lo que es más, excepciones específicas de sus objetos.

Devolver un código de error de un método no obliga al desarrollador a probarlo; provoca una excepción
en un método que obliga al desarrollador a tratarlo. De lo contrario, se mostrará un mensaje
desagradable del sistema operativo.
Normalmente, un método que puede obtener resultados diferentes devuelve un código de error que permite
disociarlos. Por ejemplo, un método de apertura de archivo devuelve una información diferente, según el
resultado de su ejecución. A continuación, nada obliga al desarrollador a probar este código de retorno. Una
operación de lectura se puede lanzar mientras el archivo no esté abierto. Con el mecanismo de las
excepciones, el error bloquea la ejecución del programa si el desarrollador no ha previsto ninguna operación
para ello.

b. Principio de funcionamiento de las excepciones

En primer lugar hay que distinguir el lado emisor del lado operación de la excepción.

Lado emisor

Un método se está ejecutando.

Se detecta un funcionamiento incorrecto.

El método asigna un objeto de tipo (o hereda de) Throwable.

El método indica los atributos de este objeto para que el origen del error sea lo más explícito posible.

El método «provoca» el error utilizando la palabra clave throw, seguida por el objeto creado
anteriormente.

La ejecución del método se interrumpe inmediatamente con un return al código que llama.

El mecanismo busca entonces, en la pila de las llamadas que ha originado la ejecución de este
método, una operación compatible con la excepción en cuestión.

Si se encuentra una operación «ad hoc» en la pila de las llamadas, entonces se ejecuta.

Si no se encuentra ninguna operación en el programa, entonces la máquina virtual detiene


bruscamente la ejecución y muestra un cuadro de diálogo que explica que la excepción no se ha
gestionado.

En Java, un método que puede provocar una excepción durante su ejecución se debe declarar en su
prototipo.

En el siguiente ejemplo, el método es susceptible de provocar una excepción de tipo Exception. La


declaración del método lo indica con la palabra clave throws, seguida del tipo de la excepción:

public void MiMetodo(boolean SimulaError) throws Exception {...

IntelliJ IDEA nos ayuda en esta definición.

A continuación se muestra un ejemplo de clase, que contiene un método que provoca una excepción. Para
ilustrar el principio de funcionamiento de las excepciones, el contenido del método genera una excepción «en
la petición».

En la siguiente imagen, el prototipo del método no contiene la definición de la excepción que se puede
provocar e IntelliJ IDEA muestra un aviso, subrayando justamente la línea que genera la excepción.

Situar el ratón sobre la línea indicada permite ver una explicación del problema.

Hacer clic en la línea enciende una bombilla roja, cuyo menú contextual permite resolver el error (situar el
cursor en el error y pulsar en [Alt][Intro] provoca la visualización del mismo menú contextual).
Seleccionando la primera opción, IntelliJ IDEA añade automáticamente la «cláusula» para la excepción.

public void MiMetodo(boolean SimulaError) throws Exception {



if( SimulaError ){
throw new Exception();
}

}

Un método puede provocar varios tipos de excepciones. En este caso, la cláusula throws las lista todas,
separadas por comas.

public void MiMetodo(boolean SimulaError) throws Exception1, Exception2, Exception3 {

Por lo tanto, la sintaxis que permite provocar una excepción es:

throw <objeto de tipo o heredado de java.lang.Throwable>;

Lado operación

En el código que utiliza MiMetodo de un objeto MiClase, se debe prever tratar la excepción de tipo
Exception.

Cuando se produce la excepción, la ejecución del código se envía a la parte prevista para su
operación.

Hay dos soluciones que se ofrecen al desarrollador:

El código que llama MiMetodo no puede tratar la excepción porque no tiene todos los elementos para
hacerlo o se haya previsto una operación más apropiada y global para la aplicación. En este caso, es
preferible transmitir la excepción a un nivel superior, utilizando el comando (en el cuerpo del método)
o la palabra clave (en el prototipo) throws.

El código que llama MiMetodo puede tratar la excepción de manera eficaz porque tiene todos los
elementos para hacerlo y es lógico tratarla aquí.

De nuevo, IntelliJ IDEA nos ayuda en la administración de la excepción. Si se utiliza directamente MiMetodo,
IntelliJ IDEA nos indica que puede provocar una excepción y hay que saber lo que se desea hacer.

Pulsando en [Alt][Intro], descubriremos lo que IntelliJ IDEA nos ofrece para resolver el problema.

La primera opción reenvía la excepción al estado «anterior».

La segunda opción permite declarar una sintaxis de operación de la excepción alrededor de la llamada
a MiMetodo.

A continuación se muestra un ejemplo de código que presenta el mecanismo completo. Encontramos tres
clases. DemoExcepcion es el punto de entrada del programa. Instancia un objeto Demo y llama a su método
Execute. Este método instancia un objeto MiClase que se va a utilizar dos veces. La segunda vez, se va a
provocar una excepción «voluntariamente» y se recupera en el método Execute.

package demoexcepcion;

public class DemoExcepcion {

public static void main(String[] args) {
// El Main instancia un objeto de tipo Demo
// y llama a su método Execute
System.out.println("Inicio de Main");
Demo d = new Demo();
d.Execute();
System.out.println("Fin de Main");
}
}

class Demo {

public void Execute() {
System.out.println("Inicio de Execute");
MiClase mc = new MiClase();
try {
// Inicio del código situado para vigilar
// Esta línea no va a provocar ninguna excepción
mc.MiMetodo(false);
// Esta línea va a provocar una excepción
mc.MiMetodo(true);

System.out.println("Fin del bloque try");
}
catch (Excepcion ex) {
// Código llamado cuando se provoca la excepción
System.out.println("Excepción detectada");
System.out.println("Razón: "
+ ex.getMessage());
}
System.out.println("Fin de Execute");
}
}
class MiClase {

// Este método provoca una excepción
// "bajo demanda " para permitirnos
// entender el mecanismo
public void MiMetodo(boolean SimulaError) throws Exception
{
System.out.println("Inicio de MiMetodo");
if (SimulaError == true) {
// asigna y a continuación provoca una excepción "general"
throw new Exception("Error de MiMetodo");
//... la ejecución se detiene inmediatamente
//System.out.println("Nunca se llega a esta línea");
}
System.out.println("Fin de MiMetodo");
}
}

La sintaxis mínima para tratar una excepción está formada por dos partes.

La primera consiste en encadenar en un bloque try las líneas de instrucciones susceptibles de provocar
la excepción.

La segunda ofrece un bloque catch, que recibe como argumento el tipo de la excepción «capturada».

A continuación se muestra la salida en la consola asociada:

Inicio de Main
Inicio de Execute
Inicio de MiMetodo
Fin de MiMetodo
Inicio de MiMetodo
Excepción detectada
Razón: Error de MiMetodo
Fin de Execute
Fin de Main

La excepción se ha capturado correctamente y la clase Demo conserva el control de la ejecución del


programa, que va hasta el final y se cierra sin error.

Observe que la línea System.out.println(«Fin del bloque try»); no se ha ejecutado porque la operación se ha
desviado automáticamente hacia el bloque catch.

Por el contrario, observe que la línea System.out.println(«Fin de Execute»); se ha ejecutado después del
bloque catch.

Como se ha presentado antes, el código que solicita la ejecución -es decir, el método Execute de nuestra
clase Demo- no puede tratar la excepción y prefiere enviarla al nivel superior (en nuestro ejemplo: el método
main). Para esto, es suficiente con declarar que el método Execute también puede transmitir una excepción
de tipo Exception. Por supuesto, es necesario que el código del método main esté adaptado para tratar la
Excepcion...

A continuación se muestra el código modificado en consecuencia:

package demoexcepcion;

public class DemoExcepcion {

public static void main(String[] args) {
// El Main instancia un objeto de tipo Demo
// y llama a su método Execute
System.out.println("Inicio de Main");
Demo d = new Demo();
try {
d.Execute();
} catch (Excepcion ex) {
System.out.println("Exception en Main");
System.out.println("Razón: "
+ ex.getMessage());
}
System.out.println("Fin de Main");
}
}

class Demo {

public void Execute() throws Exception {
System.out.println("Inicio de Execute");
MiClase mc = new MiClase();

// Inicio del código ubicado para vigilar
// Esta línea no va a provocar una excepción
mc.MiMetodo(false);
// Esta línea va a provocar una excepción
mc.MiMetodo(true);


System.out.println("Fin de Execute");
}
}

class MiClase {

// Este método provoca una excepción
// "bajo demanda" para permitirnos
// entender el mecanismo
public void MiMetodo(boolean SimulaError) throws Exception
{
System.out.println("Inicio de MiMetodo");
if (SimulaError == true) {
// asigna y a continuación provoca una excepción "general"
throw new Exception("Error de MiMetodo");
//... la ejecución se detiene inmediatamente
//System.out.println("Nunca se llega a esta línea");
}
System.out.println("Fin de MiMetodo");
}
}

La salida por la consola queda como sigue:

Inicio de Main
Inicio de Execute
Inicio de MiMetodo
Fin de MiMetodo
Inicio de MiMetodo
Exception en Main
Razón: Error de MiMetodo
Fin de Main

El código del finally

Mirando la salida de la consola, nos damos cuenta de que la línea System.out.println(«Fin de Execute»); no
se ejecuta. Es normal porque la excepción ha detenido el desarrollo del método para ir al catch. En ocasiones,
esto puede plantear problemas cuando, por ejemplo, hay que ejecutar algunas instrucciones de «limpieza»
en el objeto que ha provocado la excepción. Para resolver el problema, vamos a mezclar las dos sintaxis y
añadir al bloque try catch un bloque finally{...}, cuyas líneas siempre se ejecutan sean cuales sean las
operaciones en el try.

package demoexcepcion;

public class DemoExcepcion {

public static void main(String[] args) {
// El Main instancia un objeto de tipo Demo
// y llama a su método Execute
System.out.println("Inicio de Main");
Demo d = new Demo();
try {
d.Execute();
} catch (Excepcion ex) {
System.out.println("Exception en Main");
System.out.println("Razón: "
+ ex.getMessage());

}
System.out.println("Fin de Main");
}
}

class Demo {

public void Execute() throws Exception {
System.out.println("Inicio de Execute");
MiClase mc = new MiClase();
try {
// Inicio del código ubicado para vigilar
// Esta línea no va a provocar excepción
mc.MiMetodo(false);
// Esta línea va a provocar una excepción
mc.MiMetodo(true);
}
catch (Exception ex) {
throw ex;
}
finally{
mc.FinUtilizacion();
System.out.println("Fin de Execute");
}
}
}
class MiClase {

// Este método provoca una excepción
// "bajo demanda" para permitirnos
// entender el mecanismo
public void MiMetodo(boolean SimulaError) throws Exception
{
System.out.println("Inicio de MiMetodo");
if (SimulaError == true) {
// asigna y a continuación provoca una excepción "general"
throw new Exception("Error de MiMetodo");
//... la ejecución se detiene inmediatamente
//System.out.println("Nunca se llega a esta línea");
}
System.out.println("Fin de MiMetodo");
}

public void FinUtilizacion() {
System.out.println("FinUtilizacion");
}
}

A continuación, la salida por la consola se convierte en:

Inicio de Main
Inicio de Execute
Inicio de MiMetodo
Fin de MiMetodo
Inicio de MiMetodo
FinUtilizacion
Fin de Execute
Exception en Main
Razón: Error de MiMetodo
Fin de Main

Aquí vemos que, incluso si la excepción se ha tratado en el método main, el método FinUtilizacion del objeto
MiClase se ha llamado gracias al bloque finally.

Por lo tanto, la sintaxis de la operación de una excepción es:

try{ <bloque que puede provocar una excepción>


catch(<objeto de tipo Exception> ){<operación de la excepción>}
finally{<bloque ejecutado que hubiera tenido o no excepción y
que hubiera tenido o no tratamiento de la excepción>}

El bloque finally no es obligatorio, pero puede proporcionar enormes servicios.

c. Soporte de varias excepciones

En función del contenido del bloque try, puede haber varios tipos de excepciones que se podrían provocar y,
en teoría, tantos bloques catch por escribir para generarlos. Afortunadamente, si la máquina virtual Java no
encuentra el tipo exacto de la excepción en sus catchs, intentará conectarse a una operación menos
específica; en el ejemplo, buscará una excepción «padre» de la que se ha provocado.

Por lo tanto, concretamente si escribe solo un catch de tipo Throwable, ha recuperado todo porque Throwable
es la superclase de todas las excepciones (consecuencia de la máquina virtual Java o de sus paquetes). Por el
contrario, va a perder precisión de diagnóstico.

También es interesante tener información específica para reaccionar de la manera más eficaz posible a la
excepción y, para esto, puede codificar varios bloques catch, unos después de otros. El orden en la lista es
importante: debe empezar a escribir los catch sobre las excepciones más específicas para continuar después
hacia las más generalistas.

Ejemplo de catch múltiple sobre una familia de excepciones


try
{
// operaciones;
}
catch (EOFException)
{
// Acciones si EOFException
}
catch (EOFException)
{
// Acciones si EOFException
}
catch (Exception e)
{
// Acciones si Exception
}

Como regla general, se va a definir en el prototipo de un método las excepciones que puede provocar
solo si el código de usuario de este método tiene la forma de gestionarlas. Se habla entonces de
excepciones «Checked». En el caso contrario, las excepciones se llaman «Unchecked» y
desafortunadamente no hay más que hacer cuando se provocan.

10. Ejercicio

a. Enunciado

Cree un proyecto «LabContYExcepciones» de tipo consola.

Cree una clase Usuario que contenga las propiedades Nombre, Apellido y Edad con las siguientes
funcionalidades:

Un constructor ad hoc debe simplificar la creación/inicialización de la clase.

La edad introducida debe estar comprendida entre 0 y 109 años y se debe informar al usuario de la
clase si la edad indicada no está dentro del límite permitido.

El método java.lang.Object.toString se debe sobrecargar para resumir el contenido de la clase.

En el programa principal (main), cree cuatro usuarios que contengan la siguiente información:

Nombre Apellido Edad

Duvinage André 45

Leclerc Fernand 51

Durand Hervé 115

Lefebvre Thierry 28

Cada creación se debe corresponder con una línea de resumen del registro mostrado en la consola.

Cualquier error de formato debe ser el objeto de una línea de error en la consola y no bloquear el
resto del programa.

A continuación se muestra la visualización buscada:

Duvinage André edad: 45


Leclerc Fernand edad: 51
Durand Hervé edad 115 incorrecta(-1)
Lefebvre Thierry edad: 28

b. Consejos

Como es imposible devolver un código de error desde un constructor, será necesario utilizar el mecanismo de
las excepciones para comunicarse con el usuario de la clase, cuando la edad no responda a la restricción
impuesta por las «especificaciones».

El lugar ideal para realizar la prueba de validez de la edad es el método setter de este atributo.

Aunque exista una excepción de la máquina virtual de Java llamada IllegalArgumentException que encajaría
perfectamente para devolver un error sobre la edad, se le sugiere crear un objeto original.

Como un error no debe bloquear el resto del programa, hay que externalizar la creación de las instancias
Usuario en un método que gestione las excepciones.

c. Corrección

package com.eni;

public class Main {
// Este método permite fabricar
// las instancias Usuario.
static Usuario AgregarUsuario( String nombre,
String apellido, int edad) {

Usuario dest = null;
// El try/catch va a actuar si los argumentos
// de la instanciación son incorrectos.
try
{
dest = new Usuario(nombre, apellido, edad);
}
catch (UsuarioExcepcion ce)
{
// Ha habido un error durante la instanciación.
System.out.println(
ce.errorMessage + "(" + ce.errorCode + ")");
}
return dest;
}

// A continuación se muestra el punto de entrada de la aplicación
// Instancia los cuatro usuarios
// y muestra el resumen de su información
public static void main(String[] args) {
Usuario duvinage =
AgregarUsuario("Duvinage", "André", 45);
if( duvinage != null )
System.out.println(duvinage.toString());
Usuario leclerc =
AgregarUsuario("Leclerc", "Fernand", 51);
if( leclerc != null )
System.out.println(leclerc.toString());
Usuario durand =
AgregarUsuario("Durand", "Hervé", 115);
if( durand != null )
System.out.println(durand.toString());
Usuario lefebvre =
AgregarUsuario("Lefebvre", "Thierry", 28);
if( lefebvre != null )
System.out.println(lefebvre.toString());
}

static class Usuario
{
private String nombre;
public String getNombre() {
return nombre;
}

public void setNombre(String nombre) {
this.nombre = nombre;
}

private String apellido;
public String getApellido() {
return apellido;
}

public void setApellido(String apellido) {
this.apellido = apellido;
}

private int edad;
public int getEdad() {
return edad;
}

public void setEdad(int edad) throws UsuarioExcepcion {

// Si la edad está comprendida en la franja
// entonces, se guarda
if (edad >= 0 && edad < 110)
this.edad = edad;
else
{
// En caso contrario, se provoca una excepción "custom"
throw new UsuarioExcepcion(
-1,
String.format("%s %s Edad %d incorrecta",
this.nombre, this.apellido, edad));
}
}

public Usuario(String nombre, String apellido, int edad)
throws UsuarioExcepcion {
setNombre(nombre);
setApellido(apellido);
setEdad(edad);
}

// El método Object.toString se toma para resumir
// el contenido del objeto
@Override
public String toString() {
return String.format("%s %s edad: %d",
this.nombre, this.apellido, this.edad);
}
}

// La clase UsuarioExcepcion extiende
// la clase Exception; es una obligación.
// Esta clase contiene un código de error y un mensaje
static class UsuarioExcepcion extends Exception {
int errorCode;
String errorMessage;

public UsuarioExcepcion(
int errorCode, String errorMessage) {
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
}

}
Las interfaces

1. Introducción
Explicar las interfaces y su interés siempre es mejor con un ejemplo concreto en el que basarse. Imaginemos
por lo tanto un programa que permita controlar un sistema domótico desde un teléfono móvil (con nuestros
smartphones siempre conectados, la locura para este tipo de aplicación es rebosante). Este programa gráfico
permitirá manejar persianas eléctricas, leer temperaturas, encender el horno, etc. En resumen, leer y escribir
los estados lógicos (verdadero o falso) y leer y escribir los valores analógicos (de 00 a ff, por ejemplo).

En este tipo de aplicaciones, es necesario no atarse a un hardware concreto. Un cambio de la tarjeta de


entradas/salidas -es decir, la tarjeta que va a leer los sensores y controlar los relés bajo demanda- debe
impactar lo menos posible en el código existente.

Y aquí es donde las interfaces de programación nos van a ayudar.

2. El contrato
Para tener éxito en nuestra independencia con respecto al hardware, hay que limitar sus relaciones a su más
sencilla expresión y «contractualizarlos».

Por analogía podemos decir que es debido a la «estandarización del Conector Jack 3.5mm estéreo» que
cualquier auricular se pueda conectar a cualquier reproductor digital. Esta famosa toma juega el rol de interfaz
entre dos piezas de hardware, que no son obligatoriamente del mismo constructor.

Limitar los enlaces a su más sencilla expresión implica listar las funcionalidades mínimas esperadas para la
tarjeta de entrada/salida. Esta lista es una especie de contrato que deberá respetar obligatoriamente el
hardware. Para retomar la analogía anterior, los fabricantes de auriculares ofrecen productos con conectores de
diámetros normalizados. Gracias a esta «interfaz», es posible la interconexión.

Por lo tanto, para este proyecto domótico, ¿cuáles son nuestras necesidades?

Es necesario poder:

leer los estados binarios sobre las entradas referenciadas: interruptores, pulsadores, sensores de
presencias,

leer los valores analógicos sobre las entradas referenciadas: sensores de temperatura, de luz o de
sonido,

controlar las salidas binarias referenciadas: persianas, horno, motor de la puerta, bomba, etc.,

controlar las salidas analógicas referenciadas: reguladores de luz, etc.

Por lo tanto, a continuación se muestra la lista mínima con las funciones que la encapsulación de una tarjeta de
entrada/salida deberá ofrecer obligatoriamente para ser compatible con nuestra aplicación:

Boolean LecturaBinaria(int numTarjeta, int numEntrada);


int LecturaAnalogica(int numTarjeta, int numEntrada);
void EscrituraBinaria(int numTarjeta, int numSalida, Boolean estado);
void EscrituraAnalogica(int numTarjeta, int numSalida, int val);

Importante: este contrato no contiene código ni datos, incluso aunque, desde Java 8, un contrato pueda
tener operaciones «por defecto», pero la presencia de código no es necesaria en nuestro ejemplo.

3. Declaración de una interfaz


Sintaxis de declaración

[Modificador de acceso] interfaz miInterfaz [:Lista


interfaces básicas]
{cuerpo de la interfaz}

La mayor parte de las veces, una interfaz se declara una vez en un archivo fuente.

Modificador de acceso: opcional si la interfaz se declara en una clase. En caso contrario, es de tipo
público o no declarado. En este último caso, la interfaz se utiliza únicamente por el paquete que la
alberga.

miInterfaz: este nombre cumple las mismas prerrogativas que las de una clase. Algunos desarrolladores
prefijan los nombres de sus interfaces por un ’I’ (ex: IBasicIO); otros no porque consideran la interfaz
como un tipo hacia el que los objetos que los implementan se pueden transtipar. En este libro, las
interfaces empezarán por una ’I’ para que el lector pueda identificarlas fácilmente.

Lista de interfaces básicas: opcional; esta lista define la interfaz o las interfaces de la que hereda la
nueva interfaz. Por lo tanto, la clase que implemente esta nueva interfaz deberá implementar TODOS
los miembros de TODAS las interfaces de la lista.

Ahora vamos a poder «envolver» nuestra lista de firmas de métodos en una interfaz que llamaremos, por
ejemplo, IbasicIO:

Package demointerfaces;

// Interfaz que contiene los métodos a soportar obligatoriamente
public interfaz IBasicIO {
// Lectura de una entrada binaria
// referenciada tarjeta + número entrada
Boolean LecturaBinaria(int numTarjeta, int numEntrada);
// Lectura de una entrada analógica
// referenciada tarjeta + número entrada
int LecturaAnalogica(int numTarjeta, int numEntrada);
// Escritura de una salida binaria
// referenciada tarjeta + número salida
void EscrituraBinaria(int numTarjeta, int numSalida,
Boolean estado);
// Escritura de una salida analógica
// referenciada tarjeta + número salida
void EscrituraAnalogica(int numTarjeta, int numSalida, int val);
}

Esta interfaz constituye el contrato que toda tarjeta deberá soportar para funcionar con nuestra aplicación.
Recuerda a una clase que contiene únicamente los métodos abstractos (tipo de métodos que estudiaremos un
poco más adelante).

Las interfaces deben seguir las siguientes reglas:

Una interfaz no puede contener ni código ni datos. Sin embargo, la versión 8 de Java ofrece la
posibilidad de implementar en la interfaz un código por defecto, que se utilizará si el objeto no
implementa el método.

Ningún modificador de acceso prefija los elementos de la interfaz; todo es de tipo public implícitamente.

Una clase puede implementar tantas interfaces como quiera, mientras que solo pueda extender de una
única clase.

Una interfaz puede en sí misma heredar de una o de varias interfaces.

4. Implementación
Imaginemos ahora que el fabricante Electro276 ofrece en su catálogo una tarjeta de entrada/salida llamada
E276, cuyas características electrónicas son compatibles con nuestras necesidades. Electro276 entrega con
esta tarjeta una DLL clásica, que permite su explotación usando programación en lenguaje C.

Una DLL (Dinamic Link Library) es un formato de archivos de librerías de software. Concretamente, una DLL
contiene código ejecutable desde las aplicaciones, que pueden conectarse dinámicamente. Ofreciendo una DLL
el constructor de la tarjeta ofrece funciones del nivel superior para controlar la tarjeta sin tener que mostrar
sus secretos de fabricación.

Para utilizar la tarjeta E276 con nuestra aplicación, es suficiente con construir una clase que implementará la
interfaz IBasicIO y que invocará indirectamente el código C de la DLL.

Se habla aquí de invocación indirecta porque la DLL Windows considerada como «nativa» no es
directamente accesible desde la máquina virtual Java. Si algún día se enfrenta a este problema, sepa que
existen soluciones, principalmente con las API JNI y JNA.

public class E276 implements IBasicIO {


@Override
public Boolean LecturaBinaria(int numTarjeta, int numEntrada){
//...
return true;
}

@Override
public int LecturaAnalogica(int numTarjeta, int numEntrada){
//...
return 0;
}

@Override
public void EscrituraBinaria(int numTarjeta, int numSalida,
Boolean estado){
//...
}

@Override
public void EscrituraAnalogica(int numTarjeta, int numSalida,
int val){
//...
}
}

Si una clase que se declara como que aloja una interfaz no implementa todos los miembros de la interfaz,
entonces se producirán errores de compilación.

Sintaxis de declaración

class MiClase implements IMiInterfaz [, IMiInterfaz2 ...] {


//...
}

IMiInterfaz es la interfaz que se deberá implementar en MiClase.

[, IMiInterfaz2 ...] eventualmente una lista de otras interfaces que MiClase deberá implementar.

5. IntelliJ IDEA y las interfaces


IntelliJ IDEA nos ayuda en la implementación de las interfaces. Compruébelo usted mismo:

Cree un nuevo proyecto DemoInterfaces.

Cree una interfaz IBasicIO en el paquete demointerfaces.

package com.eni;

// Interfaz que contiene los métodos a soportar obligatoriamente
public interfaz IBasicIO {
// Lectura de una entrada binaria
// referenciada tarjeta + número entrada
Boolean LecturaBinaria(int numTarjeta, int numEntrada);
// Lectura de una entrada analógica
// referenciada tarjeta + número entrada
int LecturaAnalogica(int numTarjeta, int numEntrada);
// Escritura de una salida binaria
// referenciada tarjeta + número salida
void EscrituraBinaria(int numTarjeta, int numSalida,
Boolean estado);
// Escritura de una salida analógica
// referenciada tarjeta + número salida
void EscrituraAnalogica(int numTarjeta, int numSalida,
int val);
}

Cree una clase E276 que implemente la interfaz IBasicIO.

package com.eni;

public class E276 implements IBasicIO {


}

Haga [Alt][Intro] en el nombre de la clase E276 y seleccione Implement methods.

Entonces, IntelliJ IDEA crea en la clase E276 todos los métodos de la interfaz IBasicIO. Por lo tanto, no habrá
error de compilación y el código es minimalista.

package com.eni;

public class E276 implements IBasicIO {

@Override
public boolean LecturaBinaria(int numTarjeta, int numEntrada) {
return false;
}

@Override
public int LecturaAnalogica(int numTarjeta, int numEntrada) {
return 0;
}

@Override
public void EscrituraBinaria(int numTarjeta, int numSalida,
boolean estado) {

}

@Override
public void EscrituraAnalogica(int numTarjeta, int numSalida,
int val) {

}
}

6. Representación UML de una interfaz


El diagrama de clases de nuestro proyecto es el siguiente:

Observe la manera en la que la implementación (también llamada «realización» en lenguaje UML) de la interfaz
se representa en la clase E276.

7. Interfaces y polimorfismo
Interfaces y polimorfismo habitualmente van juntos en la programación orientada a objetos. Gracias al contrato
IBasicIO, nuestra aplicación considera todas las tarjetas de entrada/salida como objetos de tipo IBasicIO. Solo
podrá saber que se trata de una tarjeta E276 en el momento de su instanciación.

package com.eni;

public class DemoInterfaces {

public static void main(String[] args) {

// Como la clase E276 implementa
// la interfaz IBasicIO, es posible
// escribir la siguiente línea:
IBasicIO miTarjeta = new E276();
// ...
// En el resto del código, se considera
// un objeto de tipo IBasicIO
// y podemos llamar a sus métodos
// sin tener que preocuparnos del objeto real.
miTarjeta.EscrituraAnalogica(0, 5, 0x5e);
// ...
}

}

8. Ejercicio

a. Enunciado

Para resaltar este concepto, vamos a codificar una aplicación de consola que sepa utilizar diferentes medios
de comunicación para transferir los datos. En función de la infraestructura disponible, el usuario podrá elegir
entre Ethernet, Wifi y 4G.

Cada medio de comunicación se encapsulará en una clase que implementa una interfaz llamada IBaseCom,
que contiene los siguientes métodos:

Conectar.

Escribir.
Leer.

Desconectar.

Para simplificar el código, ninguno de los cuatro métodos recibirá ningún argumento y no devolverán nada.

b. Consejos

Creación de la interfaz

Cree un nuevo proyecto de tipo consola llamado TPcomun.

Añada una interfaz llamada IBaseCom que contenga los cuatro métodos mencionados anteriormente.

Creación de las tres clases de implementación

Tres clases van a implementar IBaseCom en sus aspectos específicos respectivos.

La implementación de estos cuatro métodos visualizará una línea en la consola que llama al medio de
comunicación y la acción relacionada con el método.

Ejemplo

4G - Conectar

Cree una clase llamada _4G.

Haga implementar la interfaz IbaseCom por la clase _4G.

Utilice el asistente de IntelliJ IDEA para generar automáticamente los métodos IBaseCom en la clase
_4G ([Alt][Intro] en el nombre de la clase Implement methods).

Sustituya el código insertado por un comando de visualización de consola, que llama al medio de
comunicación encapsulado y la acción solicitada.

Ejemplo

System.out.println("_4G-Conectar");

Repita la operación para las clases Wifi y Ethernet.

Codificación de la aplicación

La aplicación debe visualizar un menú que ofrece los tres medios de comunicación. En función de la elección,
se instanciará un objeto y se grabará como tipo IBaseCom. El código llamará a los métodos Conectar, Enviar,
Recibir y, a continuación, Desconectar antes de repetir en el bucle la visualización del menú.

Codifique un bucle de tipo do while.

En este bucle, muestre un menú que presente las tres opciones de comunicación y una opción para salir
de la aplicación.

A continuación, declare una referencia a un objeto de tipo IBaseCom que valga null.

Gestione la entrada del usuario y codifique un switch.

En función de la opción elegida, instancie la clase asociada y actualice la referencia IBaseCom en la


salida del switch y, si la referencia no es null, llame a los cuatro métodos.

Ejecución

Una vez compilada, podemos ejecutar nuestra aplicación y probar su funcionamiento. En un primer momento,
se muestra el menú principal. A continuación, el usuario elige y valida el tipo de medio de comunicación que
desea utilizar. En función de su selección, el encadenamiento de las llamadas se traza en la consola y, a
continuación, la aplicación repite dentro del bucle la visualización del menú.
c. Corrección

Contenido de IBasicCom.java:

package com.eni;

// Prototipo de todos los métodos
// que deberán implementar las clases
// de comunicación.
public interfaz IBaseCom {

void Conectar();
void Escribir();
void Leer();
void Desconectar();

}

Contenido de _4G.java:

package com.eni;

public class _4G implements IBaseCom {

@Override
public void Conectar() {
System.out.println("4G-Conectar");
}

@Override
public void Escribir() {
System.out.println("4G-Escribir");
}

@Override
public void Leer() {
System.out.println("4G-Leer");
}

@Override
public void Desconectar() {
System.out.println("4G-Desconectar");
}

}

Contenido de wifi.java:

package com.eni;


public class Wifi implements IBaseCom {

@Override
public void Conectar() {
System.out.println("Wifi-Conectar");
}

@Override
public void Escribir() {
System.out.println("Wifi-Escribir");
}

@Override
public void Leer() {
System.out.println("Wifi-Leer");
}

@Override
public void Desconectar() {
System.out.println("Wifi-Desconectar");
}

}

Contenido de Ethernet.java:

package com.eni;


public class Ethernet implements IBaseCom {

@Override
public void Conectar() {
System.out.println("Ethernet-Conectar");
}

@Override
public void Escribir() {
System.out.println("Ethernet-Escribir");
}

@Override
public void Leer() {
System.out.println("Ethernet-Leer");
}

@Override
public void Desconectar() {
System.out.println("Ethernet-Desconectar");
}

}

Para terminar, el contenido de Main.java:

package com.eni;
import java.util.Scanner;

public class Main {

public static void main(String[] args) {

boolean fin = false;
do
{
// Visualización del menú
System.out.println("Menú Principal");
System.out.println("1: 4G");
System.out.println("2: Wifi");
System.out.println("3: Ethernet");
System.out.println("");
System.out.println("0: Salir");
System.out.println("Su elección: ");

// Referencia a una instancia de tipo IbaseCom
IBaseCom baseCom = null;
// Gestión de la opción
int elección = -1;

Scanner sc = new Scanner(System.in);
switch( sc.nextInt() )
{
case 0:
fin = true;
break;
case 1:
// Instanciación de una clase _4G
// y almacenamiento de su referencia en baseCom
// posible porque _4G es un IbaseComm
baseCom = new _4G();
break;
case 2:
// Ídem con la clase Wifi
baseCom = new Wifi();
break;
case 3:
// Ídem con la clase Ethernet
baseCom = new Ethernet();
break;
default:
System.out.println("Opción no válida. ");
break;
}
if (baseCom != null)
{ // Si la opción es válida, entonces baseCom se asigna
baseCom.Conectar();
baseCom.Escribir();
baseCom.Leer();
baseCom.Desconectar();
}
System.out.println();
System.out.println();
} while (!fin);
}
}

Este ejercicio ha establecido la implementación de una interfaz por medio de las clases y sus usos de manera
homogénea desde una aplicación.

9. Las interfaces de la máquina virtual Java


Java ofrece interfaces que permiten a sus objetos interactuar de manera sencilla en el funcionamiento general
de la máquina virtual.

Por ejemplo:

Ha creado objetos que contienen varias propiedades.

Almacena estos objetos en tablas.

Desea poder ordenar estas tablas utilizando el método estándar Java: Collections.sort.

Problema: usted y solo usted conoce los criterios de clasificación de sus objetos.

Solución: haga heredar su clase de la interfaz «normalizada Java» Comparable y desarrolle su algoritmo de
comparación en su método public int compareTo(Object t).

Ejemplo

A continuación se muestra una clase Coche que contiene dos propiedades: Constructor y Antiguedad.

Para poder clasificar varios objetos de este tipo en una tabla, la clase Coche implementa la interfaz Comparable
y por lo tanto expone su «método-contrato» compareTo.

Es en este método donde se codifica la lógica de comparación entre dos objetos.

Package comparardemo;

// La clase Coche hereda de Comparable
// por lo tanto, debe implementar el método compareTo

public class Coche implements Comparable {

private String constructor;

public String getConstructor() {
return constructor;
}


public void setConstructor(String constructor) {
this.constructor = constructor;
}

private int antiguedad;

public int getAntiguedad() {
return antiguedad;
}

public void setAntiguedad(int antiguedad) {
this.antiguedad = antiguedad;
}

// El método CompareTo contiene nuestra lógica
// de comparación entre instancias de Coche

@Override
public int compareTo(Object t) {
if( t instanceof Coche){
return constructor.compareTo( ((Coche)t).getConstructor());
}
else
return -1;
}

}

Todavía no hemos visto las colecciones, pero sepa desde ahora que, gracias a la implementación de la interfaz
Comparable, una colección de varias instancias de nuestra clase Coche se va poder a ordenar, utilizando los
métodos del nivel superior como Collections.sort.

A continuación se muestra un ejemplo de código donde descubrimos la sintaxis de construcción de una tabla,
su relleno con objetos de nuestra clase Coche y su iteración antes y después de la ordenación.

Contenido de CompararDemo.java:

Package comparardemo;

import java.util.Arrays;

public class CompararDemo {

public static void main(String[] args) {

Coche[] tabCoches = new Coche[3];
tabCoches[0] = new Coche();
tabCoches[0].setConstructor("Renault");
tabCoches[1] = new Coche();
tabCoches[1].setConstructor("Citroën");
tabCoches[2] = new Coche();
tabCoches[2].setConstructor("Peugeot");

System.out.println("Contenido no ordenado:");
for (Coche coche: tabCoches) {
System.out.println("\t"+coche.getConstructor());
}

// Solicitud de ordenación por parte del método
Arrays.sort(tabCoches);

System.out.println("Contenido ordenado:");
for (Coche coche: tabCoches) {
System.out.println("\t"+coche.getConstructor());
}
};
}

La ejecución de este código provoca la siguiente visualización:

Contenido no ordenado:
Renault
Citroën
Peugeot
Contenido ordenado:
Citroën
Peugeot
Renault
BUILD SUCCESSFUL (total time: 9 seconds)
Asociaciones, composiciones y agregaciones
En todo programa, el desarrollador se anima a diseñar clases que utilicen o contengan otras clases, que a su vez
pueden utilizar o contener otras clases, etc. Por ejemplo, un formulario (cuadro de diálogo con el usuario)
muestra diferentes controles como botones de radio, casillas de selección, campos de introducción de texto u
otras listas desplegables. El formulario y cada uno de sus controles se «encapsulan» en las clases que el
desarrollador va a asociar para alcanzar la visualización final.

Las asociaciones son más o menos fuertes. En nuestro ejemplo, la asociación es fuerte porque es el formulario
que instancia estos controles y estos mismos controles se destruirán durante su cierre. Se habla entonces de
agregación «compuesta» o más sencillamente de «composición».

Durante esta asociación el objeto Contenedor accede libremente a los miembros de tipo public de cada uno de
los objetos Contenido. De esta manera, durante su carga, nuestro formulario podrá inicializar los contenidos por
defecto de las cajas de texto, las selecciones de los botones de radio y, durante la validación, recuperar las
opciones del usuario, preguntando a cada uno de los controles.

¿Cómo permite el lenguaje Java administrar estas diferentes formas de colaboración?

Sea cual sea el grado de asociación, la clase Contenedor necesitará almacenar las referencias sobre las clases
«contenidas».

Puede haber varios objetos de los mismos tipos referenciados en la clase Contenedor. Esta pluralidad también se
expresa en UML, a través de un índice al final del enlace, indicando bien una cantidad finita o una franja posible.

En este ejemplo, un autor puede escribir uno o un número indefinido de libros (1..*) y un libro solo se asigna a
un único autor (1).

Por lo tanto, el lenguaje Java va a tener varias formas de codificación para estos diferentes tipos de asociación.

La clase Contenedor contiene una sencilla referencia a un objeto de tipo Contenido.

class Contenedor{
Contenido contenido = null;
}

Traducción UML de esta asociación:

La clase Contenedor contiene una lista de tamaño fijo de referencias a los tipos Contenido. En este caso,
es preferible un objeto de tipo tabla, que se presenta más adelante.

class Contenedor {
Contenido[] tabContenidos = new Contenido[10];
}

Traducción UML de esta asociación:


La clase Contenedor contiene una lista de tamaño indeterminado de referencias a los tipos Contenido. En
este caso, es preferible un objeto de tipo List<>, que se presenta más adelante.

class Contenedor {
List<Contenido>listaContenidos = new ArrayList<Contenido>();
}

Traducción UML de esta asociación:

Volveremos dentro de poco sobre las tablas y las colecciones genéricas.

Observe que en estos tres ejemplos se declaran los lugares para almacenar las referencias, pero los
objetos de tipo Contenido todavía no se instancian.

En el caso de una asociación «simple», la clase Contenedor no instancia los objetos a los que hace referencia.
Los recibirá «desde el exterior», por ejemplo, usándolos como argumento en uno de sus métodos.

class Contenedor
{
Contenido contenido = null;
public setContenido(Contenido contenido) {
this.contenido = contenido;
}
}

En este caso, el objeto Contenido puede sobrevivir al objeto Contenedor (salvo que alguien lo referencie en otro
lugar, por supuesto).

En el caso de una asociación «fuerte», el objeto Contenedor se encarga de crear el objeto o los objetos
Contenido. El momento de esta creación es decisión del desarrollador. Lo que importa es que el objeto se haya
creado antes de su utilización.

Primera sintaxis posible:

class Contenedor {
private Contenido contenido = new Contenido();
}

Traducción UML de esta asociación:

El objeto Contenido se crea con el objeto Contenedor. Es una manera radical de liberarse del problema de la no
disponibilidad, pero el constructor de Contenido podrá recibir información conocida únicamente durante la
ejecución.

Se trata de una forma de agregación por referencia (rombo vacío en la figura anterior) porque el elemento
Contenido se instancia por el elemento Contenedor.

Recordemos que, en el caso de una composición (rombo lleno en la figura anterior), el Contenido forma parte y
solo puede pertenecer al Contenedor. El contenido se destruirá al mismo tiempo que el Contenedor. La
composición -o agregación por valor- de objetos de tipo por referencia no es posible en Java. Al contrario de lo
que sucede en C++, una clase se instancia obligatoriamente sobre el heap y, por lo tanto, Contenedor y
Contenido siempre tendrán zonas de memoria distintas. Para que Contenido se fusione en Contenedor, tienen
que pertenecer a la familia de los valores.

Si quiere acercarse lo más posible a una composición con objetos de tipo por referencia, entonces lo debe
codificar a partir de una agregación por valor y tener cuidado con nunca transmitir al exterior las referencias de
los objetos Contenidos. De esta manera, la línea del ciclo de vida de los objetos Contenidos se corresponderá
con la de los objetos Contenedores.

Package contenedorcontenido;

public class ContenedorContenido {

public static void main(String[] args) {
// Creación de un objeto de prueba
// que va a demostrar la composición
new Prueba();
}
}

class Prueba {

public Prueba() {
// El constructor de Prueba asigna
// un objeto Contenedor y llama
// a su método MetodoContenedor
Contenedor que contiene = new Contenedor();
contenedor.MetodoContenedor();
} // se pasa este paréntesis al objeto Contenedor
// que se hace elegible para el garbage collector
}

// La clase Contenedor contiene un objeto Contenido
// inmediatamente creado con él
class Contenedor {

// Como el objeto Contenido es de tipo private,
// su referencia no saldrá de la instancia
// de Contenedor
private Contenido contenido = new Contenido();

// Contenedor puede explotar los servicios
// de Contenido durante todo su ciclo de vida
public void MetodoContenedor() {
contenido.MetodoContenido();
//...
}


// durante la destrucción de un objeto Contenedor
// no existirá ninguna referencia a su dato
// miembro de tipo Contenido que, por lo tanto, será
// candidato para el garbage collector
}


class Contenido {
public void MetodoContenido() {
//...
}
}

En este extracto de código, el miembro Contenido se crea al mismo tiempo que el objeto Contenedor. Contenido
no se puede copiar hacia el exterior porque ningún método de Contenedor devuelve su referencia y su atributo
de visibilidad, prohibiendo el acceso directo. Por lo tanto, será elegible para la destrucción tan pronto como se
destruya Contenedor.

Volveremos a la sintaxis declaración/instanciación de un objeto Contenido en un objeto Contenedor. Esta forma


de creación tiene el defecto de no poder recibir información dinámica. Si esta restricción es un problema,
entonces es suficiente con mover la instanciación a uno de los métodos de la clase Contenedor como su
constructor, que puede ser una alternativa interesante.

class Contenedor {
// El dato miembro de tipo Contenido
// no se asigna más durante la creación
// de Contenedor
Contenido contenido = null;

// Se asigna durante la llamada
// del constructor de Contenedor y de esta manera
// puede beneficiarse de una información "dinámica"
public Contenedor(String info) {
this.contenido = new Contenido(info);
}
}

class Contenido {
public Contenido(String info) {
//...
}
public void MetodoContenido() {
//...
}
}

En el ejemplo, imaginemos que el constructor de la clase Contenido espera una cadena de caracteres
«dinámica», como por ejemplo una entrada del usuario. En este caso, el encadenamiento es ideal, pero el nuevo
defecto de esta solución es que el objeto Contenido se crea de manera sistemática. En efecto, puede ser que
solo necesitemos sus servicios en determinados casos.

La siguiente solución solo crea el objeto cuando es necesario.

class Contenedor {
Contenido contenido = null;

private String info;

public Contenedor(String info) {
this.info = info;
}

public void Tratamiento() {
//...
this.contenido = new Contenido(this.info);
//...
}

public void PosteriorTratamiento() {
//...
this.contenido.MetodoContenido();
//...
}
}


class Contenido {
public Contenido(String info) {
//...
}
public void MetodoContenido() {
//...
}
}

Si utilizando un método el objeto Contenedor transmite una referencia de su objeto Contenido a otro objeto de
usuario que la almacena y la explota, entonces Contenido y Contenedor se hacen independientes. Por ejemplo,
el objeto Contenido podrá sobrevivir a la destrucción del objeto Contenedor que lo haya creado.

Si no se guarda ninguna referencia al objeto Contenido por ningún otro objeto, entonces Contenedor y Contenido
desaparecerán en conjunto.

Ahora es momento de presentar las tablas y sus codificaciones en Java.

1. Las tablas
Las tablas son sucesiones de referencias o valores ordenados de manera contigua. Una tabla contiene
elementos del mismo tipo. Las tablas tienen un tamaño fijo, declarado durante su creación. Modificar el tamaño
de una tabla durante una operación no es muy sencillo. Hay que crear una segunda con el nuevo tamaño y a
continuación copiar el contenido anterior, y para terminar agregar las nuevas entradas. Si en un programa el
tamaño de la colección puede cambiar durante la ejecución a causa de frecuentes operaciones de inserción y
eliminación, quizás es mejor utilizar objetos de colecciones, que se presentarán en la siguiente sección.

Sin embargo, la tabla es el medio más básico y rápido para contener las series de datos.

La declaración de la tabla se compone del tipo de datos que va a contener y el número de entradas que va a
soportar.

Sintaxis de creación de una tabla

Tipo [] miTabla = new tipo[tamaño];

Ejemplo

int[] tabInt = new int[10];

En este ejemplo, tabInt contiene diez enteros «listos para usar» porque el tipo int forma parte de la familia de
los tipos primitivos y no hay necesidad de otra forma de instanciación.

Contenido[] tabContenidos = new Contenido[10];

En este segundo ejemplo, tabContenidos está lista para recibir diez referencias de tipo Contenido (las de las
instancias de objetos), pero atención: estas referencias todavía no se han definido y todas las entradas de la
tabla se ponen a null durante su creación.

Cada entrada de la tabla es accesible por un índice numérico, que va desde cero al tamaño de la tabla menos
uno. En nuestro ejemplo, será posible acceder desde tabContenidos[0] a tabContenidos[9].

Cualquier tabla en Java es un objeto y, por lo tanto, forma parte de la familia de las referencias y esto es cierto
sea cual sea el tipo de datos contenido.

Por lo tanto, cualquier tabla Java hereda los métodos de Object y un atributo length que representa su tamaño.

El API Java Coleccion ofrece la clase Arrays, que expone cincuenta métodos para realizar las operaciones sobre
las tablas. Mencionamos las ordenaciones, las copias, las inicializaciones e incluso las comparaciones.

La inicialización de una tabla se puede realizar directamente en la declaración de la clase:

public class DemoTabla {



String[] miTabCadenas = new String []
{ "Cadena1", "Cadena2", "Cadena3" };
//...
}

En este extracto de código, el tamaño de la tabla no se indica porque el compilador la puede deducir durante la
definición de las entradas.

También se puede hacer en un método como el constructor de la clase, por ejemplo:

Package demotabla;

class MiClase {

// La tabla es de la familia por referencia.
// Es posible declarar de manera sencilla
// el tipo contenido sin tener que instanciarla
public String[] miTabCadenas = null;

public MiClase(){
// El constructor de la clase crea
// realmente la tabla de cadenas en memoria.
// Esta tabla tiene tres entradas
// que el constructor sustituye
miTabCadenas = new String[3];
miTabCadenas[0] = "Cadena1";
miTabCadenas[1] = "Cadena2";
miTabCadenas[2] = "Cadena3";
}
}

public class DemoTabla {

public static void main(String[] args) {

MiClase miClase = new MiClase();
// ...
}
}

Recorrer el contenido de una tabla se puede hacer de manera sencilla con la siguiente sintaxis:

for( <tipo> var: <tabla>)


{ ... }

Ejemplo

public class DemoTabla {



public static void main(String[] args) {

MiClase miClase = new MiClase();
for(String s: miClase.miTabCadenas)
{
System.out.println(s);
}
// ...
}
}

Esta primera sintaxis solo permite un recorrido hacia adelante únicamente.

También se puede utilizar una segunda sintaxis del bucle for para recorrer una tabla. La lectura de cada
entrada se hará con el operador [].

public class DemoTabla {



public static void main(String[] args) {

String[] miTabCadenas = new String[3];
miTabCadenas[0] = "Cadena1";
miTabCadenas[1] = "Cadena2";
miTabCadenas[2] = "Cadena3";

for(int i=0; i<miTabCadenas.length; i++) {

System.out.println(miTabCadenas[i]);

}
// ...
}
}

La sintaxis for contiene tres partes, separadas por ";".

for(<inicialización>;<expresión_condición>;<modificación>)
{}

La primera fija el valor de inicio, a saber: una variable local al bucle de tipo int y que llama i al valor cero.

La segunda representa una prueba que se realizará al inicio de cada vuelta del bucle, incluida la primera. Aquí,
i se compara a una variable simbólica llamada length, que hay que sustituir por el tamaño de nuestra tabla:
miTabCadenas.length. Si la prueba se cumple, entonces se ejecuta el contenido del bucle. En caso contrario, la
ejecución pasará al siguiente.

La última parte contiene la operación que se realizará al final del bucle: aquí el incremento de la variable local
i.

Representación algorítmica del bucle for:

for(int i=0; i<miTabCadenas.length; i++) {



System.out.println(miTabCadenas[i]);

}

Durante la ejecución de la iteración, la variable i va a cambiar desde cero hasta el «tamaño de la tabla -1»
porque la condición es «mientras que i sea menor que el tamaño de la tabla». Por lo tanto, la variable i se
puede utilizar como índice de acceso a la tabla.

La sintaxis es:

<miTabla>[índice]

El contenido de una tabla se puede modificar durante la ejecución de la aplicación.

public void PruebaTab3(int index, String nueva) {


miTabCadenas[index] = nueva;
}

Como ya hemos visto, es imposible agregar o eliminar elementos de la tabla directamente.

Una tabla se puede pasar como argumento a un método y también devolver por un método.
El ciclo de vida de una tabla se corresponde con el de un objeto de tipo por referencia: si nadie más la utiliza,
entonces es objetivo del garbage collector. Si la tabla contiene tipos por valor, entonces desaparecerán con
ella. Si contiene tipos por referencia, entonces sus entradas se convierten a su vez en elegibles para la
destrucción si nadie más los referencia. Aquí podemos imaginar la complejidad de las operaciones del garbage
collector.

Si durante su desarrollo sabe exactamente en qué momento un objeto se convierte en inútil, entonces es
oportuno poner su referencia a null. Mejorará la legibilidad de su código y simplificará las operaciones del
garbage collector.

Una tabla puede tener varias dimensiones. El siguiente ejemplo instancia una tabla de dos dimensiones. Cada
dimensión tiene 10 entradas.

int[][] tabPyt = new int[10][10];

El siguiente código permite rellenar las entradas de esta tabla de dos dimensiones realizando el producto de los
índices.

public class DemoTabla {



public static void main(String[] args) {
int[][] tabPyt = new int[10][10];
for (int i = 0; i < 10; i++){

for (int j= 0; j < 10; j++){

tabPyt[i][j] = i * j;
}
}
}
}

También es posible utilizar una tabla de tablas de tamaños variables.

Declaración de una tabla de tablas

<tipo>[][] nombreTab = new <tipo>[<tamaño>][];

Ejemplo:

int[][] miTabDeTab = new int[10][];

A continuación, hay que asignar una tabla para cada entrada de la tabla. Las tablas asignadas pueden ser de
diferentes tamaños.

public class DemoTabla {



public static void main(String[] args) {
int[][] miTabDeTab = new int[10][];
for (int i = 0; i < miTabDeTab.length; i++) {
miTabDeTab[i] = new int[5];
//.
}

}
}

Para acceder a los elementos de la tabla, hay que utilizar la siguiente sintaxis:

nombreTab[<índice tab principal>][<índice tab secundaria>]

Ejemplo

miTabDeTab[2][3] = 7;

2. Las colecciones
Hemos visto que, si el programa debe poder insertar, eliminar y añadir elementos en una lista, entonces la
utilización de colecciones se hace obligatoria.

El API Java contiene varias interfaces (Collection, List, Set, SortedSet, NavigableSet, Queue, etc.), que
exponen métodos de operaciones de colecciones estructuradas y ofrece clases de implementaciones asociadas.

Una colección reúne elementos que pertenecen a una misma familia.

Como en POO, hay que tener cuidado con tipar lo más posible los objetos manipulados. Es una buena práctica
que la colección esté en sí misma fuertemente tipada. Por ejemplo, una colección de objetos Articulo no debe
ofrecer métodos que solo sepan manipular objetos de tipo Articulo. Por lo tanto, vemos muy rápido que en un
proyecto completo habría que desarrollar muchas clases de colecciones tipadas, con contenidos muy similares.
Esto rápidamente se convertiría en algo fastidioso y en una fuente de errores.
Afortunadamente, desde Java 5 existen los tipos genéricos (concepto ya soportado por C++).

El principio de las clases genéricas es definir el tipo de los objetos que hay que almacenar como argumento del
mismo tipo que la colección. De esta manera, el desarrollador se puede beneficiar de las interfaces y clases
Collections de Java, inmediatamente adaptadas a los tipos de objetos que desee coleccionar.

Sintaxis de declaración de una colección

ArrayList<miTipo> nombreColeccion = new ArrayList<miTipo>();

Con miTipo entre dos corchetes angulares, para el tipo de elemento de la colección.

Ejemplo

Package democolecciones;

import java.util.ArrayList;

public class DemoColecciones {

public static void main(String[] args) {
ArrayList<String> miColeccionCadenas
= new ArrayList <String>();
// ...
}

}

Este extracto de código instancia una colección de String (por lo tanto, fuertemente tipada). La clase ArrayList
-que forma parte del paquete java.util- se utiliza muy habitualmente para administrar las colecciones.

Observe que, para simplificar la instanciación de las colecciones, no es necesario recordar el tipo alojado
con new. Entonces, se sustituye por la secuencia <> que se conoce por el nombre «operador diamante».

Package democolecciones;

import java.util.ArrayList;

public class DemoColecciones {

public static void main(String[] args) {
ArrayList<String> miColeccionCadenas
= new ArrayList <>();
// ...
}

}

Si el tipo almacenado no se define, entonces la colección ArrayList contendrá objetos de tipo Object. Como
todos los objetos Java que heredan de la clase Object, la colección podrá contener cualquier cosa.

package democolecciones;

import java.util.ArrayList;

class Articulo{
public int referencia;
public String nombre;
}


public class DemoColecciones {

public static void main(String[] args) {

// Creación de un objeto Articulo
Articulo.articulo = new Articulo();
artículo.nombre = "Su nombre";
artículo.referencia = 1002;

// Creación de una lista no tipada
ArrayList miColecObjetos
= new ArrayList();

// Aquí es posible escribir
// cualquier tipo
miColecObjetos.add(123);
miColecObjetos.add("Hello");
miColecObjetos.add(true);
miColecObjetos.add(articulo);
}
}

Tan pronto como el objeto ArrayList se declara fuertemente tipado, el compilador comprueba que el tipo de
entrada en la colección es compatible. En la siguiente imagen, el compilador rechaza insertar 123 en la
colección porque es un entero y no una cadena.

La utilización de colecciones genéricas simplifica y aporta fiabilidad al código. También optimiza la ejecución
porque no es necesario realizar ninguna conversión en modo lectura o escritura de datos. En todos los métodos
de la colección, el argumento T se sustituye por el tipo almacenado.

Por ejemplo, la clase ArrayList<T> implementa un método public void Add(T item);.

Si en una clase Contenedor declaramos una colección de tipo ArrayList<T> de objetos Contenido...

class Contenedor{

ArrayList<Contenido> listaContenidos = new ArrayList<Contenido>;
//...
}

... entonces el método public void Add(T item); se transformará para la instancia listContenidos en void void
Add(Contenido item);.

Este principio, que consiste en poner como argumento el tipo utilizado por una entidad de la operación,
se usa mucho en Java. Lo encontramos en la declaración de las clases, así como en la de las interfaces y
también como argumento de determinados métodos.

Volvamos a las colecciones genéricas y veamos las principales interfaces propuestas por el API Java.

(fuente - sitio web de Oracle: http://docs.oracle.com/javase/tutorial/collections/interfaces/index.html)

La interfaz Collection es la interfaz básica de la jerarquía de las colecciones del API Java. No existe
implementación real de esta interfaz.

La interfaz Set hereda de la interfaz Collection y administra las listas sin repeticiones posibles. La
implementación de esta interfaz más habitualmente utilizada es la clase HashSet. La interfaz SortedSet retoma
los principios de la anterior, añadiendo la noción de ordenación.

La interfaz List hereda de la interfaz Collection y permite administrar listas ordenadas sin rechazar los
duplicados. La clase ArrayList es la implementación de la interfaz List utilizada más frecuentemente.

La interfaz Queue también hereda de la interfaz Collection y añade métodos de inserción, extracción e
inspección, para permitir un modo de explotación de tipo FIFO (First In-First Out: primero en entrar, primero
en salir), que se presenta más adelante. LinkedList es la implementación más utilizada de la interfaz Queue.

La interfaz Dequeue también hereda de la interfaz Collection y añade métodos de inserción, extracción e
inspección, para permitir modos de funcionamiento de tipo FIFO o LIFO (Last In-First Out). ArrayDeque es la
implementación más utilizada de la interfaz Dequeue.

Una segunda rama del API empieza por la interfaz Map. El principio de funcionamiento de este tipo de objeto
es asociar claves únicas (por lo tanto sin duplicados) a los valores. A cada clave solo se le puede asociar un
único valor. HashMap es la implementación más utilizada de la interfaz Map.

a. ArrayList<E> y LinkedList<E>

ArrayList y LinkedList son dos clases concretas que implementan la interfaz List. La clase ArrayList<E> es
una colección muy parecida a la del tipo tabla clásica. Se utiliza cuando el programa no hace «muchas»
inserciones, pero la rapidez de acceso a las celdas es importante. En el almacenamiento utilizado de manera
interna por la clase de tipo tabla, las celdas están contiguas y permiten de esta manera el acceso directo a un
índice particular.

El siguiente extracto de código muestra la instanciación de un objeto de tipo ArrayList de Integer. El método
Add permite agregar directamente valores a la lista.

En la clase ArrayList<T> que soportan la interfaz Iterable<E>, es posible una iteración con una sintaxis de
tipo for(<tipo> <var>: <list>). A continuación, el contenido de la lista se modifica dinámicamente antes de
recorrerse de nuevo, esta vez usando un bucle for clásico.

// Creación de una colección


ArrayList <Integer> list = new ArrayList <>();
list.add(400);
list.add(5);
list.add(28);

// Es posible recorrer la colección
// en sentido "adelante", sencillamente
// usando un bucle for
for (Integer item: list) {
System.out.println(item);
}

Salida por la consola correspondiente:

400
5
28

Como se muestra en el siguiente extracto de código, podemos agregar a un objeto de tipo List<T> el
contenido de una tabla y el objeto List<T> puede devolver una tabla (tipo System.Array).

// Creación de una colección


ArrayList <Integer> list = new ArrayList <>();
list.add(400);
list.add(5);
list.add(28);

// Copia de una tabla en la colección
Integer[] entrada = {1, 2, 3, 4, 5};
list.addAll(Arrays.asList(entrada));

// Es posible recorrer la colección
// en sentido "adelante" de manera sencilla
// usando un bucle for
for (Integer item: list) {
System.out.println(item);
}

Salida por la consola correspondiente:

400
5
28
1
2
3
4
5

Hay otros métodos y propiedades implementados en las clases Arrays y Collections. Se puede acceder a la
documentación en línea de Oracle, a través de la URL https://docs.oracle.com/javase/9/docs/api/overview-
summary.html. Aporta a los desarrolladores una descripción de las clases y ejemplos de utilización.

La clase LinkedList<E> es una colección basada en una lista encadenada. En una lista encadenada, cada
celda contiene, además del elemento alojado (por referencia o por valor), las referencias a las celdas anterior
y siguiente.
Con una asociación como esta, las inserciones, cambios de orden y eliminaciones son muy rápidas porque se
trata de una cuestión de conexiones con los miembros Anterior y Siguiente. Por el contrario, el rendimiento
de un acceso directo a un índice dado es peor que para una tabla clásica porque el método debe recorrer la
colección. Sin embargo, el método get de acceso por índice se ofrece en la clase LinkedList.

LinkedList<Integer> miListaEncadenada = new LinkedList<Integer>();


miListaEncadenada.add(1);
miListaEncadenada.add(2);
miListaEncadenada.add(3);
miListaEncadenada.add(4);
miListaEncadenada.add(5);

System.out.println(miListaEncadenada.get(3));

b. Queue<T> y Stack<T>

La interfaz Queue<T> expone un juego de métodos para administrar una colección tipo FIFO. Este tipo de
colección se utiliza cuando, por ejemplo, la información se debe encolar temporalmente, porque su operación
no se puede realizar de forma inmediata. Cuando la operación se inicia, entonces el orden de lectura se debe
corresponder con el orden de entrada en la fila. Por este motivo se llama FIFO, es decir, first in first out.
Tendremos ocasión de volver sobre este tipo de objeto. La clase LinkedList implementa la interfaz Queue.

La clase Stack<T> es una colección diseñada para un funcionamiento de tipo LIFO, que es el inverso del
modelo FIFO. Imagine que se debe lavar una pila de platos; el último de la fila será el primero en el
fregadero: last in first out.

c. HashMap<K, V>

Un HashMap o diccionario es una colección de valores a los que podemos acceder de manera instantánea,
usando una clave. Por ejemplo, si queremos conocer rápidamente el número de cualquier mes del año,
podemos construir un diccionario que tenga como claves la lista de los meses y, como valores, los enteros de
1 a 12. Por supuesto, la clase HashMap<K, V> está adaptada para este tipo de uso.

HashMap<String, Integer> losMeses = new HashMap<>();


// Proceso de llenado de las parejas clave - valor
losMeses.put("Enero", 1);
losMeses.put("Febrero", 2);
losMeses.put("Marzo", 3);
losMeses.put("Abril", 4);
losMeses.put("Mayo", 5);
losMeses.put("Junio", 6);
losMeses.put("Julio", 7);
losMeses.put("Agosto", 8);
losMeses.put("Septiembre", 9);
losMeses.put("Octubre", 10);
losMeses.put("Noviembre", 11);
losMeses.put("Diciembre", 12);

// Utilización del diccionario:
System.out.println("El número del mes de Junio es "
+losMeses.get("Junio"));

Salida por la consola:


El número del mes de Junio es 6

d. Los iteradores

El bucle for que se ha presentado con anterioridad para recorrer la colección no es la panacea. En POO,
siempre se persigue la abstracción. Puede ser interesante poder cambiar de tipo de colección -como por
ejemplo, pasar de un ArrayList<T> a un LinkedList<T>- sin tener que retocar el código de todos los métodos
que los recorren.

Las colecciones heredan de la interfaz Collection (como ArrayList y LinkedList), soportan la interfaz
Iterable<E>. Sus instancias pueden enviar un objeto que permita recorrerlos de manera análoga. Estos
objetos se llaman enumeradores o iteradores. Soportan la interfaz Iterator<E> y se pueden pasar como
argumentos a los métodos, como en el siguiente extracto de código:

// Independientemente de su naturaleza, tan pronto como una colección


// pueda devolver un objeto Iterator, entonces se puede
// recorrer de la misma manera.
public void MuestraColeccion(Iterator<String> miIterador){

while(miIterador.hasNext()){
String elemento = miIterador.next();
System.out.println(elemento);
}
}

A continuación, se muestra un extracto de código que rellena diferentes tipos de colecciones y utiliza el
método genérico definido con anterioridad para visualizar su contenido.

ArrayList<String> myArrayList = new ArrayList<>();


myArrayList.add("Enero");
myArrayList.add("Febrero");
myArrayList.add("Marzo");
myArrayList.add("Abril");
myArrayList.add("Mayo");
myArrayList.add("Junio");
myArrayList.add("Julio");
myArrayList.add("Agosto");
myArrayList.add("Septiembre");
myArrayList.add("Octubre");
myArrayList.add("Noviembre");
myArrayList.add("Diciembre");
MuestraColeccion(myArrayList.iterator());

LinkedList<String> myLinkedList = new LinkedList<>();
myLinkedList.add("Lunes");
myLinkedList.add("Martes");
myLinkedList.add("Miércoles");
myLinkedList.add("Jueves");
myLinkedList.add("Viernes");
myLinkedList.add("Sábado");
myLinkedList.add("Domingo");
MuestraColeccion(myLinkedList.iterator());

Stack<String> st = new Stack<>();
st.add("jjj");
st.add("kkk");
st.add("lll");
MuestraColeccion(st.iterator());

El método MuestraColeccion realmente ha hecho la abstracción del tipo de la colección fuente.

3. Ejercicio

a. Enunciado

Cree un nuevo proyecto de tipo consola.

En el main, cree una tabla que contenga los días de la semana.

Muestre cada día utilizando un bucle de tipo for(... :...).

Cree una colección de tipo ArrayList.

Vuelva a copiar en la colección el contenido de la tabla anterior, en orden inverso.

Muestre la colección.

Elimine de la colección las entradas que contengan Martes y Jueves (utilice lastIndexOf y remove).

Muestre la colección.

Cree un diccionario con las claves de tipo String y los valores de tipo Integer (java.util.HashMap<String,
Integer>).

Vuelva a copiar en este diccionario el contenido de la tabla de los días, dando como valores el número
correspondiente a cada día (1 para Lunes, etc.).
Utilice el método containsKey para comprobar la presencia de Miércoles y, a continuación, muestre su
valor utilizando el método get del diccionario.

Salida por la consola asociada:

Contenido de la tabla:
Lunes
Martes
Miércoles
Jueves
Viernes
Sábado
Domingo
Contenido de la colección:
Domingo
Sábado
Viernes
Jueves
Miércoles
Martes
Lunes
Contenido de la colección sin Martes ni Jueves:
Domingo
Sábado
Viernes
Miércoles
Lunes
En la semana, el Miércoles es el día número 3

b. Corrección

/*
Tpcolecciones
*/

package com.eni;

import java.util.ArrayList;
import java.util.HashMap;



public class TPcolecciones {

/**
* @param args the command line arguments
*/
public static void main(String[] args) {

// Creación de una tabla que contenga
// los días de la semana.
String [] miTablaDeLosDias = new String[7];
miTablaDeLosDias[0] = "Lunes";
miTablaDeLosDias[1] = "Martes";
miTablaDeLosDias[2] = "Miércoles";
miTablaDeLosDias[3] = "Jueves";
miTablaDeLosDias[4] = "Viernes";
miTablaDeLosDias[5] = "Sábado";
miTablaDeLosDias[6] = "Domingo";

// Mostrar cada día utilizando un bucle for(...:...)
System.out.println("Contenido de la tabla: ");
for(String dia: miTablaDeLosDias){
System.out.println(dia);
}

// Creación de una colección de tipo ArrayList
ArrayList<String> miListaDeLosDias = new ArrayList<>();

// Volver a copiar el contenido en orden inverso
// de la tabla anterior en la colección
for(int i=miTablaDeLosDias.length-1; i>=0; i--){
miListaDeLosDias.add((miTablaDeLosDias[i]));
}

// Mostrar la colección
System.out.println("Contenido de la colección: ");
for(String dia: miListaDeLosDias){
System.out.println(dia);
}

// Eliminar de la colección las entradas
// que contienen “Martes” y “Jueves”
int indiceDiaAeliminar
= miListaDeLosDias.lastIndexOf("Martes");
if( indiceDiaAeliminar != -1) {
miListaDeLosDias.remove(indiceDiaAeliminar);
}
indiceDiaAeliminar
= miListaDeLosDias.lastIndexOf("Jueves");
if( indiceDiaAeliminar != -1) {
miListaDeLosDias.remove(indiceDiaAeliminar);
}

// Mostrar la colección
System.out.println("Contenido de la colección sin Martes ni
Jueves: ");
for(String dia: miListaDeLosDias){
System.out.println(dia);
}

// Creación de un diccionario con las claves
// de tipo String y los valores de tipo Integer
HashMap<String, Integer> miDicoDeLosDias
= new HashMap<>();

// Volver a copiar el contenido de la tabla de los días
// en este diccionario, dando como valores
// el número del día (1 para Lunes etc.)
int i=1;
for(String dia: miTablaDeLosDias){
miDicoDeLosDias.put(dia, i++);
}

// Utilizar el método containsKey
// para comprobar la presencia de Miércoles
if( miDicoDeLosDias.containsKey("Miércoles")){
// Mostrar su valor utilizando
// el método get del diccionario
int numDiasMiercoles
= miDicoDeLosDias.get("Miércoles");
System.out.println("En la semana, el Miércoles es el día
número "+numDiasMiercoles);
}
}
}
Las clases anidadas
Es posible declarar una clase dentro de otra clase. Esta funcionalidad ofrece al desarrollador otra manera más de
organizar su código. Las clases principales se guardan en los paquetes y, por lo tanto, es posible realizar
agrupaciones dentro de ellas.

La mayor parte de las veces, la clase anidada -llamada nested class o inner class- no significa nada fuera de su
clase host y su operador de visibilidad es de tipo private. A pesar de todo, es posible modificar este tipo de
acceso como public, protected o package private.

Sintaxis de una nested class

public class ClassHost {



class ClaseAnidada {

}
//.
}

La clase anidada tiene acceso a todos los miembros de la clase host. La clase host tiene acceso al resto de
los miembros de la clase anidada.

El hecho de declarar una clase dentro de otra no implica una instanciación automática de la clase
anidada durante la instanciación de la clase host. Esto sigue siendo una declaración.

La clase anidada puede ser de tipo static y, en este caso, se llama comúnmente static nested class.

Si la clase anidada no es de tipo static, se llama inner class.

Ejemplo de codificación de una clase anidada y de su clase host

Package demonestedclass;

// ClassHost contiene dos propiedades,
// la definición de "ClaseAnidada"
// y un método de prueba

public class ClassHost {

// A continuación se muestran las dos propiedades:
// una "public"
public String propiedadPublicaHost;
// y otra "private"
private String propiedadPrivadaHost;

// El constructor inicializa estas propiedades
public ClassHost() {
System.out.println("Constructor ClassHost");
propiedadPublicaHost = "Hello";
propiedadPrivadaHost = "World";
}

// A continuación se muestra el método ’public’ de Prueba.
public void PruebaNestedClass(){

// creando una instancia de ClaseAnidada
ClaseAnidada miClaseAnidada
= new ClaseAnidada();

// La instancia de ClassHost puede acceder a todos
// los miembros de la instancia de ClaseAnidada
System.out.println(
miClaseAnidada.propiedadPublicaAnidada);
System.out.println(
miClaseAnidada.propiedadPrivadaAnidada);

// La instancia de ClassHost llama al método de prueba
// de la instancia de ClaseAnidada.
// Este método recibe como argumento
// una referencia de la clase ClassHost (por lo tanto, this)
miClaseAnidada.PruebaConMiHost(this);
}

// Declaración (y no instanciación) de ClaseAnidada
// en el cuerpo de ClassHost
class ClaseAnidada {
// La propiedad public de ClaseAnidada
public String propiedadPublicaAnidada;
// La propiedad privada de ClaseAnidada
private String propiedadPrivadaAnidada;

// El constructor inicializa estas propiedades
public ClaseAnidada(){
System.out.println("Constructor ClaseAnidada");
propiedadPublicaAnidada = "Hola";
propiedadPrivadaAnidada = "mundo";
}

// Método de prueba que permite mostrar
// que la instancia de ClaseAnidada puede
// acceder a los miembros de "private" y "public"
// de la instancia de ClassHost
public void PruebaConMiHost(ClassHost miHost){
System.out.println(miHost.propiedadPublicaHost);
System.out.println(miHost.propiedadPrivadaHost);
}
}
}

Salida por la consola correspondiente:

Constructor ClassHost
Constructor ClaseAnidada
Hola
mundo
Hello
World

Este extracto de código muestra una clase host ClassHost, que instancia una clase anidada ClassNested, durante
la llamada a su método PruebaHostNested. A continuación, la instancia de esta clase anidada accede a los
miembros de su host, tanto de tipo public como de tipo private.

Si la clase anidada se declara de tipo public, entonces se convierte en utilizable desde «el exterior».
Sencillamente será necesario indicar su ruta de acceso durante la instanciación.

using System;

namespace Cap7
{
class Program
{
static void Main(string[] args)
{
new Prueba();
}

public class ClassHost
{
public class ClassNested
{
public string PropPublicClassNested { get; set; }
}

public string PropPublicClassHost { get; set; }
}
}

class Prueba
{
public Prueba()
{
Program.ClassHost.ClassNested cn
= new Program.ClassHost.ClassNested();
cn.PropPublicClassNested = "Hello";
}
}
}
Algunas diferencias con C#
Los lenguajes Java y C# son muy parecidos. Los dos son lenguajes de referencias (literal y figuradamente) y
pasar de uno a otro no presentará grandes problemas.

Si es desarrollador C#, las diferencias respecto a este capítulo son que Java no soporta:

Las estructuras: herencia de C, las estructuras son muy parecidas a las clases en C#. La principal
diferencia afecta a la memoria en la que se almacenan. Las clases se escriben en el heap y las
estructuras están en la pila (stack); por lo tanto, son de muy rápido acceso porque se declaran por el
compilador. No necesitan ninguna asignación del sistema operativo.

Las clases parciales (definidas en varios archivos fuentes): es un bien, digamos, determinado.

Los métodos parciales que tienen firmas definidas en un archivo fuente e implementaciones (opcionales)
definidas en otro: misma observación.

La sobrecarga de operadores y principalmente los [], que representan indexadores, incluso el ==, que
permite probar la igualdad entre dos objetos. Es verdad que, aunque la sobrecarga de operadores sea
opcional, el usuario de las clases C# debe comprobar si realmente son operativos, mientras que en Java
la cuestión no se plantea y se debe utilizar los métodos de prueba y de acceso correspondientes.
6. HERENCIA Y POLIMORFISMO
Entender la herencia
El mecanismo de la herencia se utiliza mucho en POO, por lo que es importante recordar su utilidad.

Heredar de una clase ayuda a «especializar» determinados comportamientos y algunas de sus propiedades,
aprovechando sus servicios básicos y, de esta manera, evitando cualquier redundancia de código.

Java y C# solo permiten una única herencia por nivel (al contrario que C++), pero es posible heredar de una
clase en sí misma ya heredada y así sucesivamente, para formar una jerarquía de clases que partan de la más
global hasta la más detallada.

Ejemplo de jerarquía de clases

Por defecto, una clase puede servir de «padre» a varias clases. Sin ninguna duda, el mejor ejemplo es
java.lang.Object, que es la clase raíz de todos los tipos -y por lo tanto, de todas las clases- de Java. Recordemos
que esta herencia está implícita y no necesita ninguna definición particular.
Codificación de la superclase (clase de base) y de su subclase
(clase heredada)
El código de una clase define las reglas de su eventual herencia.

1. Prohibir la herencia
En primer lugar ¿es deseable hacer una clase «heredable»? Si el análisis demuestra que no, entonces se debe
utilizar la palabra clave final en la definición de la clase, para prohibir cualquier herencia.

Sintaxis de declaración de una clase «final»

[visibilidad] final class NombreClase


{
//...
}

Ejemplo de clase «final»

public final class Director {


//...
}

Como se muestra en la siguiente captura, IntelliJ IDEA rechaza compilar una clase que extiende de la clase
final Director:

La clase Java String es una clase de tipo «final».

Una clase que no contiene la palabra clave final en su definición se considera extensible.

2. Definir los miembros heredables


Una superclase elige a sus miembros «transmisibles», gracias a sus atributos de accesibilidad. De esta manera,
las clases heredadas (subclases) tendrán el permiso de utilizar y redefinir los miembros de tipo protected y, por
supuesto, los miembros de tipo public. Respecto a los miembros de tipo private de la superclase,
permanecerán inaccesibles para sus subclases.

3. Sintaxis de la herencia
Sintaxis de declaración de la herencia de una clase

[visibilidad] [final] class NombreSubClase extends NombreSuperClase {


//...
}

Es la palabra clave extends la que precede el nombre de la superclase.

Ejemplo de clase heredada

class ClaseHija extends ClaseMadre {



//...
}

Representación UML:
4. Explotación de una clase heredada
La instanciación de un objeto de tipo ClaseHija se hará de manera «clásica», usando la instrucción new
ClaseHija();.

Los miembros accesibles por la referencia del objeto serán los miembros de tipo public de ClaseHija y también
los miembros de tipo public de ClaseMadre.

La notación apuntada se utiliza para acceder a los miembros deseados.

Ejemplo de código que muestra una herencia y su utilización desde un programa

package com.eni;


// Definición de una clase que
// sin ser "final", se puede convertir
// en una "superclase".
public class ClaseMadre {
// con una propiedad ’public’
// accesible a todos
public String PublicPropClaseMadre;
// una propiedad ‘protected’
// accesible a ella y a sus heredadas
protected String ProtectedPropClaseMadre;
// y una propiedad ’private’ accesible
// tanto por ella como sus hijas
private String PrivatePropClaseMadre;
}

// Definición de una clase heredada
// de la superclase ClaseMadre
class ClaseHija extends ClaseMadre {
// con propiedades public, protected y private
public String PublicPropClaseHija;
protected String ProtectedPropClaseHija;
private String PrivatePropClaseHija;
}

// La clase Demoherencia va a ...
class Demoherencia {
// a través de su método Prueba ...
public void Prueba(){
//a mostrar la instanciación de la heredada
ClaseHija claseHija = new ClaseHija();
// y el acceso que tiene sobre todas sus propiedades
claseHija.PublicPropClaseHija
= "PublicHija";
claseHija.ProtectedPropClaseHija
= "ProtectedHija";
// y a las de su madre
claseHija.PublicPropClaseMadre
= "PublicMadre";
claseHija.ProtectedPropClaseMadre
= "ProtectedMadre";
// y no su ’private’
}
}

La clase DemoHerencia puede acceder a las propiedades protegidas de ClaseHija porque las dos clases están
en el mismo paquete.

El extracto de código anterior muestra la instanciación de un objeto de tipo ClaseHija, el acceso a los miembros
de esta clase y a los de su clase madre. Observe que, para más claridad en la fuente, los descriptores de
acceso no se han implementado.
Comunicación entre clase de base y clase heredada

1. Los constructores
Cuando una clase heredada se instancia, el constructor de su superclase se llama antes que el suyo. A
continuación se muestra un extracto de código, seguido del resultado en la consola que lo atestigua.

package com.eni;

// El punto de entrada de nuestro ejemplo
public class ClasePrincipal {

public static void main(String[] args) {

DemoHerencia dh = new DemoHerencia();
dh.Prueba();
}
}

package com.eni;

// La clase Demoherencia va...
public class DemoHerencia {
// a través de su método Prueba...
public void Prueba(){
//... a mostrar la instanciación de la heredada
System.out.println("Instanciación de una ClaseHija");
ClaseHija claseHija = new ClaseHija();
//
}
}

package com.eni;

// Definición de una clase heredada
// de la superclase ClaseMadre
public class ClaseHija extends ClaseMadre {
// con las propiedades public, protected y private
public String PublicPropClaseHija;
protected String ProtectedPropClaseHija;
private String PrivatePropClaseHija;

public ClaseHija() {
System.out.println("Contd Clase Hija");
}

}

package com.eni;

// Definición de la "superclase".
public class ClaseMadre {
// con las propiedades public, protected y private
public String PublicPropClaseMadre;
protected String ProtectedPropClaseMadre;
private String PrivatePropClaseMadre;

public ClaseMadre() {
System.out.println("Contd Clase Madre");
}
}

Salida por la consola correspondiente:

run:
Instanciación de una ClaseHija
Contd Clase Madre
Contd Clase Hija
BUILD SUCCESSFUL (total time: 0 seconds)

Parece muy claro que este «encadenamiento» ha tenido lugar sin ninguna llamada particular en el código de la
clase heredada. En el caso de constructores sobrecargados, este automatismo se puede modificar para que el
encadenamiento se realice sobre el constructor ad hoc de la clase de base. Gracias a la palabra clave super, el
heredado va a poder modificar la conexión.

La palabra clave super permite acceder a los miembros de la clase de base a partir de una clase derivada.
Sintaxis de llamada de un constructor de base desde un constructor heredado

ClaseHija([type param1, .]){


super([param.])
}

Ejemplo de código que realiza el encadenamiento con la palabra clave super

package com.eni;

// La clase Demoherencia va...
public class DemoHerencia {
// a través de su método Prueba...
public void Prueba(){
// a mostrar la instanciación de la heredada
System.out.println("Instanciación de una ClaseHija");
ClaseHija claseHija
= new ClaseHija("paraMadre", "paraHija");
// ...
}
}

package com.eni;

// Definición de una clase heredada
// de la superclase ClaseMadre
public class ClaseHija extends ClaseMadre {
// con propiedades public, protected y private
public String PublicPropClaseHija;
protected String ProtectedPropClaseHija;
private String PrivatePropClaseHija;

public ClaseHija() {
System.out.println("Contd ClaseHija");
}

public ClaseHija(String propClaseMadre,
String propClaseHija){
super(propClaseMadre);
System.out.println("Contd ClaseHija con argumento");
this.PrivatePropClaseHija
= propClaseHija;
}
}

package com.eni;

// Definición de la "superclase".
public class ClaseMadre {
// con propiedades public, protected y private
public String PublicPropClaseMadre;
protected String ProtectedPropClaseMadre;
private String PrivatePropClaseMadre;

public ClaseMadre() {
System.out.println("Contd Clase Madre");
}
// Sobrecarga del constructor de ClaseMadre
// que recibe como argumento una cadena
public ClaseMadre(String PublicPropClaseMadre){
System.out.println("Contd ClaseMadre con argumento");
this.PublicPropClaseMadre = PublicPropClaseMadre;
}
}

Salida por la consola asociada:

run:
Instanciación de una ClaseHija
Contd ClaseMadre con argumento
Contd ClaseHija con argumento
BUILD SUCCESSFUL (total time: 1 second)

La modificación de encadenamiento de los constructores se ha realizado gracias a la línea:

super(propClaseMadre);

La línea que contiene la llamada con la palabra clave super obligatoriamente debe estar en primera línea
del constructor.

La palabra clave super solo tiene sentido para las instancias de clase. Utilizar super en un método estático
provoca un error de compilación.

En C# el equivalente a la palabra clave super es base.


En C++ hay que enunciar el nombre de la clase madre, seguido de :: para provocar la ambigüedad de una
herencia múltiple.

2. Acceso a los miembros de la clase de base desde el heredado


De nuevo es la palabra clave super la que se utiliza para que un método de la clase heredada pueda acceder a
los miembros de su clase de base. No obstante, si el miembro que se va a extender tiene un nombre único en
toda la jerarquía, entonces la utilización de la palabra clave super se convierte en opcional. La utilización de la
palabra clave super tiene la ventaja de conservar un código comprensible por una parte y activar el asistente
de IntelliJ IDEA por otra, para ofrecerle todos los miembros heredados accesibles. Preste atención y juzgue
sobre esta captura de pantalla.

Evidentemente, si la clase heredada ofrece un miembro del mismo nombre y del mismo tipo, la palabra clave
super se hace obligatoria para evitar la ambigüedad.

package com.eni;

// Definición de la "superclase".
public class ClaseMadre {
// con propiedades public, protected y private
public String PublicPropClaseMadre;
protected String ProtectedPropClaseMadre;
private String PrivatePropClaseMadre;
public String MismoNombre;

public ClaseMadre() {
System.out.println("Contd Clase Madre");
}
// Sobrecarga del constructor de ClaseMadre
// que recibe como argumento una cadena
public ClaseMadre(String PublicPropClaseMadre){
System.out.println("Contd ClaseMadre con argumento");
this.PublicPropClaseMadre = PublicPropClaseMadre;
}
}

package com.eni;
// Definición de una clase heredada
// de la superclase ClaseMadre
public class ClaseHija extends ClaseMadre {
// con propiedades public, protected y private
public String PublicPropClaseHija;
protected String ProtectedPropClaseHija;
private String PrivatePropClaseHija;
public String MismoNombre;

public ClaseHija() {
System.out.println("Contd ClaseHija");
}

public ClaseHija(String propClaseMadre,
String propClaseHija){
super(propClaseMadre);
System.out.println("Contd ClaseHija con argumento");
this.PrivatePropClaseHija
= propClaseHija;
}

public void Prueba(){
super.MismoNombre = "Hello";
MismoNombre = "World";
}
}

package com.eni;

// La clase Demoherencia va...
public class DemoHerencia {
// a través de su método Prueba
public void Prueba(){
//a mostrar la instanciación de la heredada
System.out.println("Instanciación de una ClaseHija");
ClaseHija claseHija
= new ClaseHija("paraMadre", "paraHija");
claseHija.Prueba();
// ...
}
}

En este extracto de código, el método Prueba de ClaseHija mezcla el uso de super y de this para acceder a los
miembros heredados para el primero y a los miembros de la instancia para el segundo. Esta sintaxis solo es
obligatoria para la propiedad MismoNombre, que se define en las dos clases. Este caso puede parecer chocante
al principio. En efecto, ¿por qué nombrar a los miembros de manera idéntica? Sea como sea, es una práctica
muy habitual y estudiaremos el interés en el capítulo que trata el polimorfismo.

3. Métodos virtuales
«Especializando» una clase de base, el desarrollador añade nuevos miembros y también puede sustituir
determinados miembros existentes de la clase de base para conseguir comportamientos específicos. Se habla
entonces de métodos virtuales.

Al contrario de lo que sucede con C++ y C#, todos los métodos Java son virtuales por defecto.

Recordemos que, en POO, una clase heredada se puede considerar como una especie de tipo de su clase
madre. Por ejemplo, si una clase Destornillador hereda de la clase Herramienta, entonces un objeto de tipo
Destornillador se podrá considerar como una especie de objeto Herramienta. Por lo tanto, su instanciación
también se podrá guardar como una referencia, tanto del tipo Destornillador como del tipo Herramienta.

El siguiente extracto de código utiliza este mecanismo.

package demovirtual;

// Definición de una "superclase" Herramienta
public class Herramienta {
// con un atributo de tipo public
public String Precio;
}

package demovirtual;

// Definición de una clase Destornillador
// que extiende la superclase Herramienta
public class Destornillador extiende Herramienta {
// siendo ella también un atributo de tipo public
public String TipoDeDestornillador;
}

package demovirtual;


public class DemoVirtual {

public void Prueba(){

// Instanciación de un objeto Destornillador
Destornillador destornillador
= new Destornillador();
// Acceso a su miembro public
destornillador.TipoDeDestornillador
= "Estrella";

// Instanciación de un nuevo objeto Destornillador
// que se guarda en una referencia Herramienta
// Es posible porque Destornillador hereda de Herramienta
// y por lo tanto es una especie de Herramienta
Herramienta herramienta
= new Destornillador();
((Destornillador)herramienta).TipoDeDestornillador
= "Plano";
}
}

package demovirtual;


public class ClasePrincipal{

public static void main(String[] args) {
DemoVirtual dh = new DemoVirtual();
dh.Prueba();
}
}

Recordemos que una referencia (destornillador y herramienta en el ejemplo) no es más que «la dirección» del
objeto real. La asignación de memoria de un objeto heredado empieza por la parte «objeto de base», seguido
de la parte «miembros añadidos». Por lo tanto, es comprensible que la dirección de inicio de esta zona se
pueda considerar como referencia del tipo de base o como referencia del tipo heredado.

Observe que, en el ejemplo, cuando el objeto se referencia por su tipo de base, es necesario utilizar el
operador de transtipado (ClaseHeredada) para poder acceder a sus miembros.

Este transtipado es obligatorio porque queremos acceder a un dato miembro definido únicamente en la clase
heredada, a partir de una referencia sobre la superclase. Por el contrario, si la clase heredada «redefine» una
operación de la superclase, implementando un método con el mismo nombre, entonces este transtipado ya no
es obligatorio. En efecto, en lenguaje Java el acceso a los miembros de la clase de base siempre se redirige
hacia el miembro que se redefine en la clase heredada, y ello sea cual sea la referencia utilizada: superclase o
heredada.

No hay que utilizar una sintaxis particular para este tipo de funcionamiento nativo. Simplemente, justo antes
del método que va a sustituir (normalmente llamado «overrider») el método de base, es prudente escribir
@Override por dos razones:
El compilador va a comprobar que realmente va a sustituir un método existente de la superclase.... Esto
será muy útil en caso de error de entrada, como se muestra en la siguiente pantalla, en la que toString
se ha cambiado por ToString:

Segunda razón: su código será más fácil de leer.

Ejemplo de «sustitución»

@Override
public String toString() {
return "Destornillador{" + "TipoDeDestornillador=" +
TipoDeDestornillador + ’}’;
}

A continuación se muestra un ejemplo de código de sustitución y una utilización desde los dos tipos de
referencia:

package demovirtual;

// Definición de una "superclase" Herramienta
public class Herramienta {
// con una propiedad de tipo public
public String Precio;

@Override // override del método de Object
public String toString() {
return "Herramienta{" + "Precio=" + Precio + ’}’;
}

}

package demovirtual;


// Definición de una clase Destornillador
// que extiende la superclase Herramienta
public class Destornillador extends Herramienta {
// con ella también una propiedad de tipo public
public String TipoDeDestornillador;


@Override // override del método de Herramienta
public String toString() {
return "Destornillador{" + "TipoDeDestornillador=" +
TipoDeDestornillador + ’}’;
}
}

package demovirtual;


public class DemoVirtual {

public void Prueba(){

// Instanciación de un objeto Destornillador
Destornillador destornillador
= new Destornillador();
// Acceso a su miembro public
destornillador.TipoDeDestornillador
= "Estrella";

// Instanciación de un nuevo objeto Destornillador
// guardado en una referencia Herramienta
// Es posible porque Destornillador hereda de Herramienta
// y por lo tanto es una especie de Herramienta
Herramienta herramienta
= new Destornillador();
((Destornillador)herramienta).TipoDeDestornillador
= "Plano";

System.out.println(destornillador.toString());
System.out.println(herramienta.toString());
}
}

package demovirtual;


public class ClasePrincipal {

public static void main(String[] args) {
DemoVirtual dh = new DemoVirtual();
dh.Prueba();
}
}

Salida por la consola correspondiente:

run:
Destornillador{TipoDeDestornillador=Estrella}
Destornillador{TipoDeDestornillador=Plano}
BUILD SUCCESSFUL (total time: 0 seconds)

La declaración de un método en la superclase no implica una sustitución obligatoria del miembro en las
clases heredadas.

4. Métodos de tipo «final»


El diseñador de una clase puede decidir que algunos de sus métodos no se puedan redefinir en las clases
heredadas. Prefijará el método con la palabra clave final que ya conocemos, a nivel de la definición de las
clases, para prohibir la sustitución.

En el siguiente ejemplo, la superclase Herramienta contiene un método de este tipo llamado


NoSeDebeRedefinir.

package demovirtual;

// Definición de una "superclase" Herramienta
public class Herramienta {
// con una propiedad de tipo public
public String Precio;

@Override // porque override del método de Object
public String toString() {
return "Herramientas{" + "Precio=" + Precio + ’}’;
}

public final void NoSeDebeRedefinir(){
// ...
}

}

Si intentamos sustituir este método en la clase heredada (Destornillador), IntelliJ IDEA no lo permite:

Existe el opuesto a esta restricción, es decir, la obligación de que las clases heredadas tengan que sustituir un
método dado de la superclase. Veremos esto justo después de un pequeño ejercicio.
Ejercicio

1. Enunciado
Cree una nueva solución de tipo consola.

Añada una clase CuentaBancaria para representar una cuenta bancaria, con las siguientes propiedades:

Titular (String).

Número (Integer que contiene un valor único asignado a la instanciación; el primer número de
cuenta será 100).

Saldo (double).

Y los siguientes métodos:

Ingresar (permite ingresar dinero en la cuenta).

Retirar (permite retirar dinero de la cuenta).

Consultar (muestra toda la información de la cuenta).

Añada una clase CuentaBancariaRemunerada que represente a una cuenta bancaria remunerada que
hereda de la clase creada con anterioridad, para la que el constructor recibirá como argumentos el
nombre del titular y el porcentaje de remuneración de la cuenta.

Redefina el método Ingresar de CuentaBancariaRemunerada para que la cantidad ingresada se aumente


con el porcentaje de remuneración definido en el constructor. No soñemos; este funcionamiento bancario
atípico se hace únicamente para simplificar el ejercicio.

Codifique en el main una secuencia que permita comprobar el funcionamiento de las clases.

2. Corrección

package labcuentabancaria;

public class CuentaBancaria {

// Entero de tipo static que contiene
// el contador global de número de cuenta
private static Integer numCont = 100;

// Nombre del titular
private String Titular;
public String getTitular() {
return Titular;
}
public final void setTitular(String Titular) {
this.Titular = Titular;
}

// Número de cuenta de la instancia
private Integer Numero;
public Integer getNumero() {
return Numero;
}
public final void setNumero(Integer Numero) {
this.Numero = Numero;
}

// Saldo de la cuenta
private double Saldo;
public double getSaldo() {
return Saldo;
}
public void setSaldo(double Saldo) {
this.Saldo = Saldo;
}
// Método llamado durante un depósito en la cuenta
public void Ingresar(double credito) {
Saldo += credito;
}

// Método llamado durante un reintegro desde la cuenta
public void Retirar(double debito) {
Saldo -= debito;
}

// Devuelve el resumen de la cuenta, sustituyendo
// el método toString de la clase Object
@Override
public String toString(){
return String.format(
"Cuenta n°%d Titular %s Saldo %.2f euros",
Numero, Titular, Saldo);
}

// Constructor de la clase CuentaBancaria
// que contiene el nombre del titular
// y asigna un número de cuenta único
public CuentaBancaria(String titular) {
setTitular(titular);
setNumero(CuentaBancaria.numCont++);
}
}

package labcuentabancaria;


public class CuentaBancariaRemunerada extends CuentaBancaria {

// Porcentaje de remuneración de la cuenta remunerada
private double PorcentajeRemuneracion;
public double getPorcentajeRemuneracion() {
return PorcentajeRemuneracion;
}
public final void setPorcentajeRemuneracion(double
PorcentajeRemuneracion) {
this.PorcentajeRemuneracion = PorcentajeRemuneracion;
}

// Constructor que recibe como argumentos
// el nombre del titular y el porcentaje de remuneración.
public CuentaBancariaRemunerada(
String titular, double porcentajeRemuneracion) {
super(titular);
setPorcentajeRemuneracion(porcentajeRemuneracion);
}

// Aquí se redefine el método Retirar de la superclase
// para tener en cuenta
// un porcentaje de remuneración "inmediato"
// (atención: funcionamiento muy diferente
// a la vida real...)
@Override // permite al compilador comprobar
// que exista un método Retirar
// en la superclase
public void Ingresar(double credito) {
super.Ingresar(credito * (1 + PorcentajeRemuneracion / 100));
}

}

package labcuentabancaria;

public class ClasePrincipal {

public static void main(String[] args) {

// Creación de una cuenta "clásica" para Víctor
CuentaBancaria cb1 = new CuentaBancaria("Víctor");
// Ingreso de 1000€
cb1.Ingresar(1000);
// Impresión del recibo
System.out.println(cb1.toString());

// Retirada de 200€
cb1.Retirar(200);
// Impresión del nuevo recibo
System.out.println(cb1.toString());

// Creación de una cuenta "remunerada" al 2% para Andrés
CuentaBancariaRemunerada cb2
= new CuentaBancariaRemunerada("Andrés", 2);
// Ingreso de 500€ inmediatamente aumentado un 2%
cb2.Ingresar(500);
// Impresión del nuevo recibo
System.out.println(cb2.toString());

}

}

Salida por la consola:

run:
Cuenta n°100 Titular Víctor Saldo 1000,00 euros
Cuenta n°100 Titular Víctor Saldo 800,00 euros
Cuenta n°101 Titular Andrés Saldo 510,00 euros
BUILD SUCCESSFUL (total time: 1 second)

Como ya sabe, los módulos de una aplicación Java normalmente se entregan en un archivo .jar, que no es otra
cosa que un archivo .zip. Vamos a provechar este ejercicio para pedir a IntelliJ IDEA que lleve nuestros binarios
a este tipo de archivos.

Para esto, seleccione la opción Project Structure del menú File y seleccione Artifacts.

A continuación, haga clic en el botón + y seleccione la adición de un JAR.

Desencadene a continuación la compilación del .jar desde el menú Build - opción Build Artifacts.

Encontrará el resultado de la compilación (labcuentabancaria.jar) en su directorio


(…)\labcuentabancaria\out\artifacts. Utilizaremos este archivo un poco más adelante.
Las clases abstractas
Puede suceder que una superclase contenga métodos imposibles de implementar porque no tengan ningún
sentido sin un mínimo de especialización.

Por ejemplo, una clase de base FormaGeometrica ofrece un método virtual Diseñar. A continuación, esta clase se
extiende con las clases Triangulo, Rectangulo y Circulo. Cada una va a sustituir e implementar su propio método
Diseñar.

Por lo tanto, la implementación del método Diseñar en la clase de base FormaGeometrica no tiene ningún
sentido, porque cada forma es específica y a nivel de FormaGeometrica esta forma es abstracta.

En este caso, ¿por qué definir el método Diseñar en FormaGeometrica?

Por supuesto, esto está relacionado con el polimorfismo. Imagine que está construyendo una aplicación de
diseño y esta aplicación administra una serie de formas geométricas cuidadosamente definidas y guardadas por
el usuario. Puede administrar una lista de objetos de tipo Triangulo, una lista de objetos de tipo Rectangulo, etc.
Buena suerte, porque esto pronto se convertirá en algo bastante pesado. Construyendo una lista de objetos de
tipo FormaGeometrica, puede rellenarla de objetos de tipo Triangulo, Rectangulo y Circulo. ¿Para qué? Porque
los tres extienden la superclase FormaGeometrica y cualquier clase derivada puede implícitamente convertirse en
un objeto de su clase de base. Por lo tanto, todo se simplifica. Cuando el contenido de la lista se debe diseñar, es
suficiente con acceder a cada objeto -de tipo FormaGeometrica- y llamar a su método Diseñar. Gracias al
mecanismo de virtualización que se ha estudiado con anterioridad, es la implementación específica la que se
llamará automáticamente. Por lo tanto, hay que definir el método Diseñar en FormaGeometrica para permitir
este funcionamiento.

¿Por qué definir una clase abstracta en lugar de una interfaz?

En efecto, en el ejemplo anterior la clase FormaGeometrica se hubiera podido sustituir por una interfaz. Es
preferible utilizar una clase abstracta cuando el análisis muestre que es necesario un conjunto de
implementaciones de métodos, común a todas las clases. Recordemos que una interfaz no contiene código
(incluso si desde Java 8 esta regla está un poco equivocada) y, por lo tanto, si quiere mezclar métodos
abstractos y métodos reales, la solución que se impone es la clase abstracta. Si por el contrario no hay código
compartido, la clase abstracta, que no contiene más que los métodos abstractos, se sustituirá por una interfaz.

Una clase se declara abstracta cuando uno de sus métodos es abstracto.

Es imposible instanciar directamente una clase abstracta con la palabra clave new. Esto provoca un error
de compilación.

Sintaxis de declaración de una clase abstracta

<visibilidad> abstract class NombreClaseAbstracta {



abstract visibilidad tipo retorno NombreMetodo ([argumentos]);
//...
}
El polimorfismo

1. Entender el polimorfismo
En programación orientada a objetos, el polimorfismo permite a una clase heredada presentarse en una
operación como su clase de base o como una de sus interfaces. Gracias a la virtualización de los métodos, la
operación que llama al método de base se «enruta» en la clase heredada. De esta manera, se obtiene una
«especialización» de la operación.

Cualquier clase derivada puede implícitamente convertirse en un objeto de su clase de base.

En lenguaje Java, la virtualización, por defecto automática, se ha detallado en las páginas anteriores. Por un
lado, permite a la clase de base definir qué métodos se pueden especializar por sus heredadas y, por otra, a su
heredada o sus heredadas especializar los métodos.

En lenguaje Java, cada objeto es «polimorfo». Se puede considerar como su propio tipo o como el de su clase
de base y, por saltos sucesivos, como el tipo raíz de todos los tipos, es decir, java.lang.Object.

2. Explotación del polimorfismo


La mejor programación es aquella que incentiva la menor dependencia entre los módulos. Una arquitectura
compuesta de loosely coupled modules (módulos débilmente acoplados) es muy apreciada porque permite la
evolución de las capas independientemente del funcionamiento global. Lo que ayer aparecía como un bloque
monolítico, se convierte en algo fraccionado en capas (arquitectura n-tier con tier en el sentido de capa), que
se pueden repartir en ubicaciones geográficas totalmente diferentes.

El intercambio de los módulos es posible gracias al polimorfismo. Cada capa acepta módulos que respetan la
compatibilidad, además de una lista de comportamientos obligatorios. Entonces, las instancias de clases muy
tipadas de los objetos «compatibles» con las interfaces contractuales y la operación principal se limitan a
instanciar e interconectar las instancias compatibles con estas interfaces.

Cualquier objeto que implemente una interfaz, se puede convertir en el tipo de esta interfaz.

En sus clases, son preferibles los miembros de tipo por referencia de interfaces, en lugar de los miembros
de tipo por referencia de objetos. Haga que su código esté abierto a las evoluciones de sus componentes.

3. Los operadores instanceof y ()


Los operadores instanceof y () permiten comprobar y considerar los objetos de base como objetos heredados,
fuertemente tipados. Se debe reflejar su uso. Si prototipa un método con un argumento débilmente tipado
(como java.lang.Object) y tiene que realizar una operación específica para un tipo heredado dado, entonces
utilice el operador instanceof para comprobar la pertenencia a la familia y, a continuación, el operador de
transtipado () para crear una nueva referencia sobre el tipo heredado y, de esta manera, simplificar su
codificación.

No dude en volver a la sección getClass, .class y el operador instanceof del capítulo Los tipos en Java, donde ya
se abordó esta noción de polimorfismo. En el ejemplo, el método Mantenimiento espera un objeto de tipo
Vehiculo que a continuación se va a analizar para que se llame a la operación adaptada.

public class MantenimientoVehiculos {



public void Mantenimiento(Vehiculo v) {

// El objeto ¿va a ser creado como moto?
if( v.getClass() == Moto.class ) {

// Sí. Por lo tanto, no se ha creado
// como VehiculoAmotor
assert(v.getClass() != VehiculoAmotor.class);

// pero sigue siendo
// una instancia de VehiculoAmotor
assert( v instanceof VehiculoAmotor );

// como una instancia de Vehiculo
assert( v instanceof Vehiculo );

// y evidentemente una instancia de Moto
assert( v instanceof Moto );
}
}
}
Recuerde: instanceOf comprueba si un objeto se puede considerar como un tipo dado, mientras que
getClass devuelve el tipo «nativo» del objeto.
7. COMUNICACIÓN ENTRE OBJETOS
El evento: estar a la escucha
Como sucede con los sistemas operativos orientados a eventos, sus objetos seguramente tendrán que estar a la
escucha de otros objetos. Por ejemplo, su formulario gráfico estará «atento» a las acciones del ratón para poder
actuar inmediatamente a las peticiones del usuario. Su formulario no sabe cuándo el usuario va a hacer clic en
uno u otro componente y, por otra parte, será el propio componente el que lo llame para informarle del cambio
de estado.

Un objeto que capta información la puede difundir a los «clientes», que inicialmente se hayan suscrito a su
lista de difusión.

La POO siempre está muy próxima a la realidad. Se suscribe a su revista preferida como muchos otros. Los
periodistas escriben artículos que se agrupan en ediciones del periódico. Regularmente, se le envía este
periódico. En cualquier momento, puede detener su suscripción y suscribirse a otro medio.

En programación es lo mismo; habitualmente tendrá que implementar los mecanismos de gestión de listas de
suscriptores y difusión.
El pattern Observador

1. Aspectos generales
Esta problemática era recurrente y la «cuadrilla de los cuatro» ha diseñado un «patron de diseño» (design
pattern) para resolverlo.

Este pattern, llamado Observador, ofrece una solución de codificación para una relación de un (observado)
respecto a varios (observadores), utilizando el acoplamiento (interdependencia) más débil posible. Esta
solución se puede utilizar directamente en lenguaje Java gracias a la clase java.util.Observable y a la interfaz
java.util.Observer.

Desde JDK 9, la clase java.util.Observable ha sido «obsoletizada», lo que significa que se puede continuar
utilizando con este JDK, pero va a desaparecer en las versiones futuras. Evidentemente existen soluciones de
sustitución, pero la simplicidad de utilización de la clase java.util.Observable hace que siga siendo la
herramienta de aprendizaje ideal para establecer la comunicación entre los objetos Java.

A continuación se muestra la representación UML de este tipo de relación. Los nombres de las clases, interfaces
y miembros no se corresponden con los del API Java porque es una representación genérica del design pattern.

La clase Observable y la interfaz IObservador se diseñaron a la vez, durante el desarrollo de un objeto que
tenía que comunicar sus cambios de estado a sus suscriptores.

Más adelante, usted desarrolla un programa que debe, entre otras cosas, tratar las notificaciones de este
objeto. Este objeto se comercializa desde hace mucho tiempo y funciona perfectamente.

La comunicación entre estos dos objetos -desarrollados en momentos diferentes- podría ser posible gracias al
dúo ganador ’interfaz y polimorfismo’. En efecto, la clase Observable no conoce su objeto, pero en
contraposición, su objeto puede implementar la interfaz IObservador que ha sido proporcionada por el
desarrollador del objeto Observable.

Cuando su objeto se va a guardar en el Observable, no se va a declarar con su tipo nativo, sino como instancia
que soporta la interfaz IObservador. Por lo tanto, incluso si Observable ignora todo de su objeto, sabe que
implementa obligatoriamente un método muy particular: el que se define en IObservador. Por lo tanto, lo va a
conservar como instancia de IObservador y va a poder llamar directamente a un método (su método) en su
objeto para notificarle sus cambios. Es a la vez sencillo y eficaz.

2. Implementación en lenguaje Java


El API Java ofrece directamente una clase Observable, que también se llama java.util.Observable. La clase
Observable no es una clase abstracta. Esto quiere decir que se puede instanciar y utilizar directamente. En lo
que sigue, es preferible extenderlo (heredarlo) para aplicar los mecanismos expuestos con anterioridad. Sin
embargo, es previsible la utilización de la clase Observable autónoma instanciada que sirva de unión entre su
objeto de publicación y su suscriptor o sus suscriptores.

El API Java también ofrece el equivalente de la interfaz IObservador, con el nombre de java.util.Observer.

Por lo tanto, esta pareja de objetos se convierte en un medio genérico para que «observables» y «observados»
se puedan comunicar fácilmente, y esto para el bien de todos los desarrolladores.

Este mecanismo también funciona en C#, pero será necesario desarrollar la clase Observable y la interfaz
IObservador asociadas a su objeto porque C# no tiene equivalentes. C# aboga por un sistema de llamada
directa entre objetos, permitidos por un mecanismo event/delegate también con buen rendimiento, que evita
la implementación de una interfaz.
En C++, su objeto observable también deberá administrar sus listas de suscriptores porque no existe solución
clave en main. Además, este lenguaje no soporta las interfaces. La parte que describe la comunicación entre
las dos partes se deberá definir en una clase abstracta.

Suscripción/cancelación suscripción

Por lo tanto, el suscriptor o los suscriptores se tendrán que guardar en la clase observable para recibir las
notificaciones. Si se encarga del diseño de una clase «de difusión», entonces su trabajo es muy sencillo porque
con el simple hecho de extender la clase Observable del API Java agrega a sus objetos un conjunto de métodos
clave en mano, para administrar las suscripciones y cancelación de las suscripciones de los «clientes».

A continuación se muestra la lista de los métodos de la clase Observable del API Java que permite administrar
las suscripciones:

public void addObserver(Observer o)

public void deleteObserver(Observer o)

public void deleteObservers()

public int countObservers()

Los dos primeros métodos se utilizan por los clientes. Observe que el argumento es Observer y que Observer
es una interfaz. Por lo tanto, sus clientes tendrán que implementar esta interfaz antes de poder guardarse en
su objeto de difusión. Entonces se convertirán en tipos Observer y podrán pasar su this a los métodos de
suscripción de Observable.

Un objeto que implemente la interfaz Observer obligatoriamente debe ofrecer el método:

void update(Observable o, Object arg){


...
}

Notificación

Para notificar a sus suscriptores, la clase de difusión que extiende la clase Observable debe llamar a los
métodos setChanged y notifyObservers de manera interna.

Imaginemos que aquella detecta un cambio de estado en el hardware que encapsula. Entonces llama a estos
dos métodos para informar de ello a todos sus suscriptores. El método notifyObservers se sobrecarga y ofrece
una versión con argumento, que permite al emisor acompañar su notificación de un objeto destinado a
«comentar» el evento. El tipo de este argumento es el Object, raíz de todos los objetos Java. Por lo tanto, a
través del polimorfismo, la clase de difusión puede transmitir cualquier referencia. El método notifyObservers
realiza una iteración en la lista de suscriptores y llama el método para cada instancia obligatoriamente
implementado y accesible: update.

Observe que update recibe dos argumentos. El primero representa la instancia del Observable. En efecto, un
observador puede observar varios objetos de tipo Observable y, por lo tanto, debe poder diferenciarlos porque
el método update será el mismo para todos. El segundo argumento es el objeto que describe el porqué de la
notificación. Se ha preparado por la clase Observable durante la detección del cambio de estado. Este objeto
debe contener toda la información del evento. Por ejemplo, si es un sensor que devuelve un cambio de estado
sobre una de sus entradas, entonces tendrá que encontrar el número de la entrada en cuestión y su nuevo
estado. Si el objeto Observable llama a la versión de notifyObservers «sin argumentos», entonces el segundo
argumento que recibirán los suscriptores será una referencia a null.

A continuación se muestra la implementación del código del lado de la difusión:

Package demoobservableobserver;

import java.util.Observable;

public class ClaseObservable extends Observable {

// Identificador de instancia (para la traza)
static int count = 0;
private int Id;
public int getId() {
return Id;
}


public ClaseObservable() {
Id = count++;
}

//... Imaginemos aquí operaciones específicas
//que conducen a notificaciones a los suscriptores ...


// Un cambio interviene
// y por lo tanto la clase notifica a todos sus suscriptores
public void NotificaSuscriptores(Object arg) {

// El objeto se pasa a estado modificado
this.setChanged();
// Se advierte a todos los suscriptores
this.notificaObservers(arg);
// Al final de la notificación
// el objeto vuelve a pasar a estado no modificado

}

}

A continuación se muestra la implementación del código del lado Observador:

Package demoobservableobserver;

import java.util.Observable;
import java.util.Observer;

// La clase ClaseObservador:
// Se puede haber diseñado después
// de la clase que extiende Observable
// Para hacerla "compatible" con Observable
// es suficiente con implementar la interfaz Observer
public class ClaseObservador implements Observer {


// Identificador de instancia para la traza
static int count = 0;
int id;
public Observador() {
id = count++;
}

// Método que permite suscribir este observador
// a una instancia observable
public void SuscripcionA(Observable o) {
o.addObserver(this);
}

// El método llamado por todos los Observables
// a los que la instancia se suscribe
@Override
public void update(Observable o, Object o1) {
// que va a visualizar una línea en la consola
String mensaje
= String.format("Observador %i notificado por Observable %i",
id, ((ClaseObservable)o).getId());
System.out.println(mensaje);
}

// Método que permite cancelar la suscripción de este observador
// a una instancia Observable
public void CancelarSuscripcionDe(Observable o) {
o.deleteObserver(this);
}

}

Para terminar, a continuación se muestra un pequeño programa de prueba:

Package demoobservableobserver;


public class DemoObservableObserver {

public static void main(String[] args) {

// Creación de una instancia Observable.
ClaseObservable observable = new ClaseObservable();
// y de diez suscriptores
ClaseObservador[] tabSuscriptores
= new ClaseObservador[10];
for (int i = 0; i < tabSuscriptores.length; i++) {
// Instanciación del suscriptor
ClaseObservador cliente = new ClaseObservador();
// Guarda el suscriptor en el observable
cliente.SuscripcionA(observable);
// Guarda el suscriptor
tabSuscriptores[i] = cliente;
}

// POR NECESIDADES DE LA DEMOSTRACIÓN
// Simulación de un cambio en el Observable
observable.NotificaSuscriptores(new Object());


}

}

La clase Observador implementa la interfaz Observer, haciéndola compatible con la escucha de los objetos de
tipo Observable. El método SuscripcionA permite establecer la unión entre Observador y Observable. Para
terminar, el método update se llama cuando Observable tiene algo que decir.
La clase Program instancia un objeto Observable y le conecta diez objetos Suscriptores. Para comprobar el
funcionamiento, «simula» un cambio de estado del Observable. Observable y Observador tienen un número
asignado a sus construcciones para permitir visualizar los mensajes significativos.

A continuación se muestra la salida por la consola correspondiente:

run:
Observador 9 notificado por Observable 0
Observador 8 notificado por Observable 0
Observador 7 notificado por Observable 0
Observador 6 notificado por Observable 0
Observador 5 notificado por Observable 0
Observador 4 notificado por Observable 0
Observador 3 notificado por Observable 0
Observador 2 notificado por Observable 0
Observador 1 notificado por Observable 0
Observador 0 notificado por Observable 0
BUILD SUCCESSFUL (total time: 0 seconds)

3. Los listeners
El design pattern Observer es particularmente útil para las aplicaciones gráficas. Como hemos explicado en la
presentación de la POO, son los objetos los que notifican sus cambios al núcleo de la operación, y no la
operación que «se ejecuta en bucle» por los estados de los objetos. Su aplicación tendrá mucho mejor
rendimiento si trata el clic de ratón cuando este clic interviene en una zona significativa (como en un botón),
en lugar de vigilar permanentemente los hechos y gestos del «animal». Otro punto importante de la POO: los
objetos utilizados deben ser lo más tipados posible. En la implementación Observable-Observer, el argumento
que circula es de tipo. Object; por lo tanto, lo menos tipado.

Esto es lo que llega a los listeners.

Los listeners son Observers especializados que permiten estar a la escucha de objetos y eventos particulares.
Los listeners son interfaces que definen los intercambios de objetos fuertemente tipados, que transportan
información explotable directamente por los suscriptores.

Tomemos por ejemplo el ratón. Si necesita saber cuándo el usuario hace clic en un componente de su
aplicación, entonces debe implementar la interfaz MouseListener e implementar obligatoriamente sus cinco
métodos (aunque no sean todos útiles, porque es la ley de las interfaces). Las notificaciones recibidas se
acompañan de un objeto de tipo MouseEvent. Este objeto le informará principalmente del estado de los
botones del ratón (presionado, soltado y pulsado), la posición del cursor en la pantalla asociada a su aplicación,
las diferencias respecto a la última posición conocida, si las teclas del teclado se han pulsado durante el clic,
etc. Por lo tanto, todo esto será muy concreto para que pueda administrar de manera fina las reacciones de la
aplicación.

El código de su clase «suscriptor ratón» se amplía con tantos métodos como la interfaz MouseListener le obliga.
Si del lote solo necesita un método, entonces su código va a perder legibilidad y es una pena.

Esto sería sin contar con las clases abstractas que funcionan junto con las interfaces, proponiendo una
implementación por defecto de los métodos obligatorios. Por lo tanto, si no quiere implementar todos los
métodos de la interfaz listener, será suficiente con que su clase suscriptor extienda la clase abstracta asociada
y reutilice a su vez solo los métodos que le importan (aquí mencionamos la diferencia capital entre interfaz y
clase abstracta y el interés que podemos sacar de ella). De esta manera, su código se haría más fácil de leer.

La mayor parte de las veces, el nombre de la clase abstracta asociada a un listener es el de la interfaz con
Adapter, en lugar de Listener. De la implementación de la interfaz MouseListener pasamos a la extensión de la
clase abstracta MouseAdapter, que ya contiene -entre otros- la implementación de todos los métodos de la
interfaz MouseListener. Como en Java, todos los métodos son virtuales por defecto, y puede tomar el método o
los métodos que le interesen.

Los listeners no son exclusivos de los componentes Java preexistentes. Puede construir sus propios listeners y
sobre todo sus propios objetos Event que contengan toda la información necesaria para sus suscriptores.

4. Utilización de un listener en una aplicación gráfica


Para poner todo esto en práctica, vamos a escribir nuestro primer programa «gráfico».

El paso del modo consola al modo gráfico no es baladí. De una ejecución lineal vamos a pasar a una ejecución
orientada a eventos, que utiliza objetos Java especializados de los que hay que heredar y «adaptar» para crear
nuestra propia aplicación. La simplicidad del código es desconcertante, sobre todo para los pobres
desarrolladores de C, que tenían que escribir un centenar de líneas antes de ver un pedacito de ventana. En
contraposición, es desconcertante cuando se empieza «from scratch» (desde cero) porque se basa en los
componentes existentes, por lo que hay que asimilar los roles y los funcionamientos.

En primer lugar, hay dos librerías principales en el API Java para construir aplicaciones gráficas: AWT (Abstract
Window Toolkit) y Swing.

La primera también ha sido la primera en estar disponible. Es una especie de acceso para los programas
«Java» a las funciones gráficas disponibles en el sistema operativo. Por ejemplo, si una pantalla muestra tres
botones, entonces AWT habrá instanciado realmente tres componentes gráficos de tipo botón al «look and
feel» del sistema que aloja la máquina virtual. En consecuencia, una misma aplicación Java que funcione en
una máquina virtual Windows tendrá una representación gráfica diferente en una máquina virtual Linux. AWT
se basa en los objetos gráficos existentes y solo puede explotar un conjunto de objetos común para todos los
sistemas; de lo contrario las aplicaciones Java no se podrían ejecutar más en todas las máquinas virtuales.
Punto positivo: AWT es rápido y ligero.

Swing funciona de manera muy diferente, recreando su propio entorno gráfico. Si retomamos el ejemplo
anterior, Swing va a utilizar AWT para solicitar al sistema un rectángulo «vacío» en la pantalla en la que va a
pintar los tres botones. Como es el mismo Swing el que diseña sus botones, la representación gráfica se hace
independiente del sistema operativo host. Los programas Java Swing conservan el mismo «look and feel» en
Windows y en Linux. Inconveniente: Swing tiene más trabajo que hacer y, por lo tanto, demanda más recursos
que AWT. Esta diferencia fundamental con AWT no es realmente un problema ahora debido a la rapidez de
ejecución de los ordenadores y de los sistemas actuales. Es algo que debe considerar si desarrolla en sistemas
con capacidades reducidas.

Para nuestro pequeño programa, vamos a utilizar Swing.

Cuando se construye una HMI (Interfaz Hombre-Maquina), también llamada GUI (Graphical User Interface), se
instancian objetos que van a formarla, se configuran y se juntan. A continuación, se administran los eventos
que van a llegar durante su utilización.

La clase principal de la librería Swing que sirve de soporte para la mayor parte de las aplicaciones gráficas es la
clase JFrame.

Desde el punto de entrada main de una aplicación de tipo consola (por el momento), se instancia un JFrame,
se ajustan algunas propiedades y tenemos nuestra primera ventana Windows mostrada en pantalla.

package demojframevacio;

import javax.swing.JFrame;

public class DemoJFrameVacio {

// Método main "clásico"
public static void main(String[] args) {

// Instanciación de un objeto JFrame
// que encapsula una "ventana" Windows
JFrame ventana = new JFrame();

// Definición del título de la ventana
ventana.setTitle("Mi primera ventana Swing");

// Definición de su tamaño:
// 400 píxeles de largo y 100 píxeles de alto
ventana.setSize(400,100);
// Muestra la ventana en el centro de la pantalla
// (sea cual sea la resolución)
ventana.setLocationRelativeTo(null);
// Configuración del cierre de la aplicación
// con el clic de ratón en la cruz de la barra de título
ventana.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Y para terminar, hacer la ventana visible
ventana.setVisible(true);
}
}

Representación gráfica:

Un JFrame se compone de varios contenedores que el desarrollador utiliza para configurar su HMI. Nos vamos
a centrar en su content pane, que contiene todos los componentes gráficos y en el que vamos a agregar un
panel con algunos controles. Este panel se encapsula por la clase Swing JPanel. Por lo tanto, vamos a crear una
instancia de JPanel y ubicar dos botones y, a continuación, asociarlo al JFrame.

package demojframejpaneljbutton;

import java.awt.Color;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class DemoJFrameJPanelJButton {

// Método main "clásico"
public static void main(String[] args) {

// Instanciación de un objeto JFrame
// que encapsula una "ventana" Windows
JFrame ventana = new JFrame();

// Definición del título de la ventana
ventana.setTitle("Mi primera ventana Swing");

// Definición de su tamaño:
// 400 píxeles de largo y 100 píxeles de alto
ventana.setSize(400,100);
// Visualizar la ventana en el centro de la pantalla
// (sea cual sea la resolución)
ventana.setLocationRelativeTo(null);
// Configuración de cierre de la aplicación
// al hacer clic de ratón en la cruz de la barra de título
ventana.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

// Instanciación de un JPanel
JPanel pan = new JPanel();
// Elección de su color de fondo
pan.setBackground(Color.yellow);

// Instanciación y a continuación añadir al JPanel
// dos botones
JButton b1 = new JButton("Botón 1");
pan.add(b1);
JButton b2 = new JButton("Botón 2");
pan.add(b2);

// Asociación del panel con el JFrame
ventana.setContentPane(pan);

// Y para terminar, hacer visible la ventana
ventana.setVisible(true);
}
}

Visión general de la ejecución:

Observe que los botones se han dispuesto de manera automática. Es posible modificar el orden añadiendo en
el JPanel controles como FlowLayout, BorderLayout, GridLayout, que contendrán a los botones.

Ahora que tenemos una ventana con dos magníficos botones, vamos a aplicar todo lo que hemos visto para
poder realizar las operaciones cuando se accionen.

Para esto, vamos a modificar la organización del código.

Actualmente el main instancia un JFrame, lo configura y, a continuación, lo muestra. Este código no es muy
reutilizable. El main es el punto de entrada de la aplicación. Será necesario hacer un nuevo copiar-pegar para
reutilizar nuestra bonita ventana amarilla en otro proyecto. Esto es mucho más sencillo si tenemos una clase
autónoma cuyo código fuente se pueda compartir entre varios proyectos. Esta clase autónoma extiende la
clase JFrame y además implementa una interfaz xxxlistener para «observar» los botones.

Hay varias interfaces listeners en el API Java. En primer lugar, vamos a utilizar el más sencillo de ellos, que es
ActionListener. ActionListener permite ser notificado cuando sobre un control se realiza una acción
«significativa». Por ejemplo, para un botón se trata de un clic con el botón izquierdo del ratón o pulsar en la
barra de espacio del teclado cuando el botón tiene el foco.

A continuación se muestra el código de nuestra clase llamada VentanaAmarilla:

package demoactionlistener;

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class VentanaAmarilla extends JFrame implements
ActionListener {

// Implementar ActionListener obliga a tener
// el método actionPerformed en la clase.
// Este método se llamará cuando haya una acción
// significativa sobre un control
@Override
public void actionPerformed(ActionEvent ae) {
// Como es un método "global" hay que empezar
// por saber qué control está en el origen de la
// notificación. Para esto, se pregunta a ActionEvent
// que acompaña la llamada.
if( ae.getSource() == b1)
System.out.println("b1 ha sido pulsado");
else if( ae.getSource() == b2)
System.out.println("b2 ha sido pulsado");
}

private JButton b1;
private JButton b2;

public VentanaAmarilla(){
// Definición del título de la ventana
setTitle("Mi primera ventana con actionPerformed");

// Definición de su tamaño:
// 400 píxeles de largo y 100 píxeles de alto
setSize(400,100);
// Visualización de la ventana en el centro de la pantalla
// (sea cual sea la resolución)
setLocationRelativeTo(null);
// Configuración de cierre de la aplicación
// con un clic de ratón en la cruz de la barra de título
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

// Instanciación de un JPanel
JPanel pan = new JPanel();
// Elección de su color de fondo
pan.setBackground(Color.yellow);

// Instanciación y, a continuación, añade al JPanel
// dos botones
b1 = new JButton("Botón 1");
pan.add(b1);
b1.addActionListener(this);
b2 = new JButton("Botón 2");
b2.addActionListener(this);
pan.add(b2);

// Asociación del panel con el JFrame
setContentPane(pan);

// Y para terminar, hacer la ventana visible
setVisible(true);
}
}

El main de la aplicación ahora es muy simple: instancia sencillamente un objeto VentanaAmarilla. Como
cualquier operación de configuración, se encuentra en el constructor de la clase y no hay ninguna otra llamada.

package demoactionlistener;

public class DemoActionListener {

public static void main(String[] args) {
VentanaAmarilla fj = new VentanaAmarilla();
}

}

Visión general de la ejecución con las trazas en la ventana Output:


Ejercicios

1. Ejercicio 1

a. Enunciado

Este ejercicio consiste en escribir un programa que cree varias ventanas (de tipo JFrame) identificadas por un
número en sus barras de título y que «trace» los eventos «presionar» y «soltar» del ratón en cada una de
estas ventanas. Indicaremos cada evento mostrando en la salida por la consola de IntelliJ IDEA un mensaje
que precise su naturaleza (presionar o soltar), el número de ventana y las coordenadas del puntero del ratón
en el momento del evento.

Para esto, implementaremos un listener más especializado que actionListener: utilizaremos mouseListener.

b. Corrección

Cree un nuevo proyecto en IntelliJ IDEA y llámele LabMouseListener.

Añada una clase al proyecto llamada Ventana.

Haga que la clase Ventana extienda la clase JFrame (utilice el asistente IntelliJ IDEA, que va a agregar
la directiva de importación del paquete, si activa [Alt][Intro] en la palabra JFrame).

Añada a la clase Ventana la implementación de la interfaz MouseListener, beneficiándose también del


asistente IntelliJ IDEA, que va a agregar de golpe los cinco métodos obligatorios.

Elimine el contenido de estos cinco métodos.

Añada un constructor a la clase Ventana para:

Administrar la numeración única de las ventanas, gracias a un dato miembro de tipo entero static y
un segundo dato miembro de tipo entero instanciado.

Definir las dimensiones de la ventana por defecto, gracias al método setBounds.

Guardarse como MouseListener gracias al método addMouseListener.

Definir el comportamiento de la aplicación respecto al clic en la cruz de la barra de título.

En el método mousePressed, muestre en la ventana de salida de IntelliJ IDEA el tipo de acción, el


número de la ventana implicada y las coordenadas del puntero. Las coordenadas son accesibles desde el
objeto MouseEvent que se pasa como argumento.

Vuelva a hacer lo mismo para el método mouseReleased.

En la clase LabMouseListener que contiene el main, instancie cuatro objetos Ventana.

A continuación se muestra el código de la clase Ventana:

package labmouselistener;

import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.JFrame;

public class Ventana extends JFrame implements MouseListener {


// Da accesibilidad a todas las instancias
// y permite contar las ventanas activas
private static int numeroTotalDeVentanasActivas = 0;
// Número de la ventana asignada en el constructor
private int numeroVentanaDeEstaInstancia;

// Constructor
public Ventana() {
// Gestión de la numeración de las ventanas
numeroTotalDeVentanasActivas++;
numeroVentanaDeEstaInstancia
= numeroTotalDeVentanasActivas;
// Contenido de la barra de título
setTitle("Ventana número "
+ numeroVentanaDeEstaInstancia);
/// Definición de las dimensiones de la ventana
setBounds(10, 20, 300, 200);
// Guardar como MouseListener
addMouseListener(this);
// Gestión del cierre de la aplicación
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Activación de la ventana
setVisible(true);
}

// Método llamado con el clic: no utilizado
@Override
public void mouseClicked(MouseEvent me) {
}

// Método utilizado cuando el clic se presiona
@Override
public void mousePressed(MouseEvent me) {
System.out.println("mousePressed en Ventana número "
+ numeroVentanaDeEstaInstancia
+ " en X:" + me.getX() + " Y:" + me.getY());
}

// Método utilizado cuando el clic se suelta
@Override
public void mouseReleased(MouseEvent me) {
System.out.println("mouseReleased en Ventana número "
+ numeroVentanaDeEstaInstancia
+ " en X:" + me.getX() + " Y:" + me.getY());
}

// Método llamado cuando el cursor del ratón
// entra por encima de la ventana: no utilizado
@Override
public void mouseEntered(MouseEvent me) {
}

// Método llamado cuando el cursor del ratón
// sale de la ventana: no utilizado
@Override
public void mouseExited(MouseEvent me) {
}

}

A continuación se muestra el código de la clase LabMouseListener:

package labmouselistener;

public class LabMouseListener {


public static void main(String[] args) {

new Ventana();
new Ventana();
new Ventana();
new Ventana();

}
}

2. Ejercicio 2

a. Enunciado

La implementación de la interfaz MouseListener en la clase Ventana ha declarado cinco métodos, de los


cuales tres no se utilizan. En este ejercicio esto no es muy grave, pero en un código más largo puede
perjudicar su legibilidad.

Hemos visto que, para resolver este problema, el API Java ofrece una clase abstracta MouseAdapter, que
contiene las implementaciones por defecto de los métodos de la interfaz MouseListener.

En este ejercicio 2, debe modificar el código del ejercicio 1 para utilizar la clase abstracta MouseAdapter.

Atención: la clase Ventana ya extiende la clase JFrame, y recuerde que es posible extender varias clases a la
vez en Java.
b. Corrección

Hay varias soluciones a este problema. La que se propone utiliza una tercera clase, MiListenerRaton, que va a
hacer el enlace entre la clase abstracta MouseAdapter y la clase Ventana para respetar las reglas de herencia.

MiListenerRaton extiende la clase abstracta MouseAdapter, lo que le permite reutilizar solo los métodos que
necesita: en la ocurrencia mousePressed y mouseReleased.

Por el contrario, MiListenerRaton no tiene contenido gráfico; es la clase Ventana la que hereda de JFrame y,
por lo tanto, ella misma puede pretender guardarse como un MouseListener. Pero, para hacer esto, hay que
pasar al método addMouseListener un objeto que implemente la interfaz MouseListener.

Esto es bueno, MiListenerRaton lo hace.

Por lo tanto, el constructor de la clase Ventana va a instanciar un objeto MiListenerRaton y pasarlo como
argumento a addMouseListener.

Ahora, hay que enviar las notificaciones de ratón en la clase Ventana.

Esto es muy sencillo. Vamos a pasar en el constructor de MiListenerRaton una referencia a la ventana destino
para que MiListenerRaton pueda acceder a los métodos de Ventana. En el código de corrección, los métodos
de Ventana que tratan sobre el ratón tienen el mismo nombre que en MiListenerRaton, pero esto no es
obligatorio.

package labmouseadapter;

import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;

// La clase Ventana extiende JFrame
// pero solo implementa MouseListener
public class Ventana extends JFrame {

// Una clase MiListenerRaton se
// define (nested) en la clase Ventana
// porque no tiene sentido sin ella.
// MiListenerRaton implemente MouseAdapter
// y retoma los dos métodos que nos interesan.
class MiListenerRaton extends MouseAdapter {

// El constructor recibe como argumento
// una referencia a la ventanaDestino
// que la va a utilizar.
public MiListenerRaton(Ventana ventana) {
this.ventanaDestino = ventana;
}
// número de la ventanaDestino escuchada
private Ventana ventanaDestino;

// La llamada al método mousePressed de MouseAdapter
// se envía a la ventana.
// Por lo tanto, la referencia se guarda
@Override
public void mousePressed(MouseEvent ev) {
this.ventanaDestino.mousePressed(ev);
}

// Ídem para mouseReleased
@Override
public void mouseReleased(MouseEvent ev) {
this.ventanaDestino.mouseReleased(ev);
}

}

// Dato accesible a todas las instancias
// y permite contar las ventanas activas
private static int numeroTotalDeVentanasActivas = 0;
// Número de la ventana asignada al contador
private int numeroVentanaDeEstaInstancia;

// Constructor
public Ventana() {
// Gestión de la numeración de las ventanas
numeroTotalDeVentanasActivas++;
numeroVentanaDeEstaInstancia
= numeroTotalDeVentanasActivas;
// Contenido de la barra de título
setTitle("Ventana número "
+ numeroVentanaDeEstaInstancia);
// Definición de las dimensiones de la ventana
setBounds(10, 20, 300, 200);

// El método addMouseListener siempre se puede utilizar
// porque forma parte de la clase Jframe
// por tanto, hereda de Ventana.
// En contraposición, espera un objeto que implemente
// la interfaz MouseListener.
// Como MiListenerRaton extiende MouseAdapter
// que a su vez implementa MouseListener
// se crea una instancia pasándole el this
// correspondiente a la referencia de nuestra instancia
// ventana para que nos la pueda recordar.
addMouseListener(
new MiListenerRaton(this));

// Gestión del cierre de la aplicación
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Activación de la ventana
setVisible(true);
}
// Método utilizado cuando el clic se presiona
public void mousePressed(MouseEvent me) {
System.out.println("mousePressed en Ventana número "
+ numeroVentanaDeEstaInstancia
+ " en X:" + me.getX() + " Y:" + me.getY());
}

// Método utilizado cuando el clic se suelta

public void mouseReleased(MouseEvent me) {
System.out.println("mouseReleased en Ventana número "
+ numeroVentanaDeEstaInstancia
+ " en X:" + me.getX() + " Y:" + me.getY());
}
}
Llamadas síncronas y asíncronas
Una noción importante afecta al modo de ejecución de un método respecto al programa que utiliza. En efecto,
determinados métodos pueden tomar un tiempo no despreciable durante sus ejecuciones. Esta noción de tiempo
es subjetiva y depende del contexto de utilización. Por ejemplo, en aplicaciones de escritorio, una ejecución de
medio segundo no es perjudicial; en el dominio industrial, no es para nada lo mismo.

El funcionamiento síncrono es el modo de ejecución por defecto. El programa que llama permanece «bloqueado»
mientras dura el trabajo del método llamado.

A continuación se muestra la representación UML tipo diagrama de secuencias de una llamada síncrona desde
una instancia Objeto1 hacia una instancia Objeto2.

Si la duración de ejecución se vuelve crítica, hay que ejecutar el método de manera asíncrona. En este caso, la
ejecución se «divide en dos», permitiendo que las dos ramas de instrucciones evolucionen en un modo
«pseudoparalelo».

A continuación se muestra la representación UML-diagrama de secuencias de una llamada asíncrona desde una
instancia Objeto1 hacia una instancia Objeto2.

La programación de ejecuciones paralelas pasa por una programación de varios flujos, que vamos a presentar en
el capítulo siguiente.
8. EL MULTITHREADING
Introducción
La programación multithread es un dominio apasionante, pero que puede convertirse rápidamente en algo muy
complejo de poner a punto. Varias ejecuciones paralelas en el centro de su aplicación deberán compartir
información, esperarse, intercambiar, etc. El éxito de una arquitectura de este tipo se basa, en primer lugar, en
un análisis sólido. Este capítulo no pretende exponer todas las posibilidades de programación multithread y sus
implementaciones en Java, sino presentar lo fundamental con la filosofía POO.
Entender el multithreading
Un proceso puede realizar operaciones largas, que van a «bloquear» la aplicación durante sus ejecuciones. Para
evitar esto, el desarrollador puede crear una especie de ruta de ejecución paralela que se va a encargar de esta
operación y, de esta manera, separarse del ejecutor principal. En este caso, el sistema operativo Windows de
Microsoft comparte muy rápidamente el tiempo de máquina entre los diferentes flujos de ejecución
(normalmente 20 ms por franja de tiempo), dando la sensación de una ejecución simultánea. Se habla de
sistema operativo con derecho preferente. El contenido de una cola de ejecución puede encadenar todas las
operaciones que desee sin preocuparse del tiempo que esto implica a nivel global.

El sistema operativo «lo interrumpirá» periódicamente para dar tiempo a la cola de ejecución siguiente y así
sucesivamente, hasta volver a ella para que retome su operación allá donde fue interrumpida.

Como ejemplo, retomamos el sensor de entrada/salida equipado con una interfaz de programación muy
resumida. El constructor nos brinda un juego de funciones que permite, entre otras cosas, leer el estado binario
de las entradas digitales. Desea desarrollar una aplicación domótica que gestione varias operaciones en paralelo,
como la iluminación, la calefacción e incluso la alarma. Con lo que se ha presentado en la interfaz
Observable/Observado, desea transformar la interfaz básica de programación en un módulo observable
adecuado, capaz de llamar a las instancias de objetos suscriptores cuando un sensor detecte un cambio de
estado. Para esto, en su observable, debe crear una operación en bucle dedicada a la lectura de cada entrada de
la tarjeta. Gracias a algunas pruebas, sabrá detectar un cambio de estado real y avisar a sus suscriptores. Este
bucle de inspección se ejecutará en un thread dedicado. Las operaciones realizadas por sus suscriptores -
llamadas operaciones de negocio porque son ellas las que reaccionan a los cambios de los estados- no se
«bloquean» por este bucle. Se van a llamar durante los cambios. Mientras se espera, podrán continuar
visualizando información, imprimir históricos o hacer cálculos.

Estos flujos de ejecuciones normalmente se llaman threads. Un thread puede tener -o no- interacciones con
otras partes de la aplicación. Si es el caso, siempre hay que tener el objetivo de que la ejecución se pueda
interrumpir por el sistema operativo en cualquier momento, incluso durante la actualización de objetos que se
dejarán en un estado transitorio hasta el ciclo siguiente. Si estos objetos se comparten sin precaución con otras
partes de la aplicación, habrá funcionamientos incorrectos provocados por estos cambios de contexto.

Los threads tienen características de funcionamiento entre las que figura una noción de prioridad. En el caso de
una adquisición de un flujo de datos, es importante no perder información. Si el sistema está muy ocupado, se
corre el riesgo de volver a lanzar el thread de adquisición más tarde, después de la saturación de la memoria de
datos del hardware y, por lo tanto, con riesgo de producirse pérdidas de datos. Hay un medio de interactuar
sobre el funcionamiento del administrador de ejecuciones, eliminando la prioridad del thread. Este cambio se
debe realizar con moderación, solo para casos muy particulares que lo justifiquen.

En definitiva, es importante distinguir thread y proceso. Cuando el usuario ejecuta un archivo con extensión
.exe, se crea un proceso. Por lo tanto, los procesos son bloques de ejecución que contienen las asignaciones de
memoria y los recursos necesarios. Los procesos están aislados y, por lo tanto, no pueden «chocar» unos con
otros. La única manera de hacer que dos procesos se comuniquen entre ellos es pasar por una IPC (InterProcess
Communication), basada en los pipes, los mailslots o los sockets.

Cada proceso se identifica por su PID (Process IDentifier) y contiene al menos un thread, llamado thread
primario, que se crea automáticamente. A continuación, se pueden añadir threads secundarios utilizando
programación. El administrador de las tareas de Windows permite visualizar la lista de los procesos activos y
también conocer el número de threads de cada uno.

Si las columnas PID y Threads no se muestran, hay que activarlas mediante un clic con el botón derecho
del ratón en una de las columnas y escogiendo Seleccionar columnas.

Los threads de un mismo proceso son internos a este proceso y, de esta manera, comparten un mismo espacio
de memoria, un mismo código, así como los mismos recursos. Cada thread tiene su propio stack y también una
zona de memoria (TLS para Thread Local Storage) utilizada para guardar automáticamente los datos durante su
periodo de inactividad.

Los procesos cargan módulos en forma de archivos DLL. La vida de estos módulos está totalmente relacionada
con la del proceso.
Multithreading y Java
La parte de encapsulación de un proceso se realiza a través de la clase java.lang.Process. Como la creación de
un proceso está estrechamente relacionada con el sistema operativo en el que funciona la máquina virtual, hay
que pasar por una clase muy especializada que se llama Runtime. Cada aplicación Java tiene de manera nativa
una instancia única sobre un objeto de tipo Runtime. Gracias a ella, usted podrá iniciar un nuevo proceso.

El siguiente extracto de código permite ejecutar el programa Windows calc.exe desde un programa Java.

package demoprocess;

import java.io.IOException;


public class DemoProcess {

public static void main(String[] args) {

// Recuperación de una referencia sobre el "runtime"
Runtime runtime = Runtime.getRuntime();

try {
// Utilización de su método exec
runtime.exec("calc.exe");
} catch (IOException ex) {
// Si la ejecución se realiza incorrectamente,
// se muestra el porqué
System.out.println(ex.getMessage());
}
}
}

Un thread de una aplicación Java se configura, administra y controla por medio de una instancia del objeto
java.lang.Thread.

Naturalmente, es posible tener varios threads en funcionamiento en una aplicación.

Como se ha explicado anteriormente, también es posible asignar un nivel de prioridad a cada thread; los threads
con prioridades más altas estarán normalmente más activos que los threads con prioridades más bajas.
Implementación de los threads en Java
Hay dos maneras principales de programar los threads en Java: extender la clase Thread o implementar la
interfaz Runnable.

1. Extender la clase Thread


Extendiendo la clase Thread y situando el código que se debe ejecutar en el método run, tomado de su cuenta,
su clase se convierte directamente en «threadable». Después de su instanciación, una sencilla llamada al
método start permite arrancar el thread y ejecutar «en paralelo» el código contenido en su método run. El
thread se detiene cuando el código del método run se ha ejecutado completamente o se produce una
excepción no administrada. Práctico, ¿verdad?

A continuación se muestra un ejemplo de código que utiliza este principio:

package demothread;


// MiClaseThread extiende la clase java.lang.Thread
public class MiClaseThread extends java.lang.Thread {

// ...
// Aquí nos imaginamos diferentes métodos y descriptores
// de acceso
// Ubicamos en el método run la operación "long"
// que se va a ejecutar en un thread instanciado
// y arrancado desde el código que llama
// (el main en este ejemplo)
@Override
public void run(){

// Traza de inicio de operación.
// Se utiliza el método de tipo static
// Thread.currentThread()
// para visualizar el nombre asignado a este thread
System.out.println("Inicio de una operación de 10 segundos "
+ "en el thread "
+ Thread.currentThread().getName());

// Aquí se simula un trabajo de 10 segundas (10 x 1000 ms)
for(int i=0; i<10; i++)
{
try {
// Utilización del método de tipo static sleep
// que espera el número de milisegundos que
// se debe esperar
Thread.sleep(1000);
}
catch (InterruptedException ex) {
// Aquí es posible insertar
// una operación que se llamará cuando
// el comando Thread.sleep se esté ejecutando
// y el Thread se interrumpe
}
// Traza testigo del trabajo en curso
System.out.println(i);
}
// Traza de fin de operación
System.out.println("Fin de una operación de 10 segundos "
+ "en el thread "
+ Thread.currentThread().getName());
// Una vez que se llega a esta línea, el thread está "muerto"
// y no se puede volver a lanzar.
}
}

Código que instancia la clase y arranca la operación:

package demothread;


public class DemoThread {


public static void main(String[] args) {

System.out.println("Creación de un nuevo thread "
+ "desde el thread "
+ Thread.currentThread().getName());
MiClaseThread miThread = new MiClaseThread();

System.out.println("Inicio del nuevo thread");
miThread.start();

System.out.println("Fin de main");
}

}

Salida por la consola asociada:

Si su clase ya es una heredada directa, no puede extender la clase Thread porque, como ya se ha explicado,
Java no soporta la herencia de varias clases por nivel. En este caso, tiene la solución de implementar la interfaz
Runnable que se presenta justo después.

Desde un punto de vista puramente orientado a objetos, el hecho de extender la clase Thread presupone
la adición de nuevas funcionalidades a su clase. En nuestro caso, estas nuevas funcionalidades afectan
más a un modo de ejecución que a una funcionalidad de negocio y esta adición se vuelve discutible. El
enfoque interfaz Runnable le permite conservar sus objetos de negocio intactos y hacer que sus métodos
costosos en tiempo se ejecuten a través de un segundo objeto especializado.

2. Implementar la interfaz Runnable


Implementar la interfaz Runnable le obliga a declarar un método run en el que situar el código que se va a
ejecutar. El programa que desea ejecutar su código pasa la instancia de su clase como argumento de
construcción de un objeto Thread. A continuación, una llamada al método start arranca la ejecución. Este modo
de funcionamiento es un nuevo ejemplo de utilización del polimorfismo. La clase Thread diseñada por los
desarrolladores Java no conoce en absoluto su clase; en contraposición, espera una instancia de objeto que
soporte la interfaz Runnable que haya implementado.

Por lo tanto, la implementación de su clase permanece concentrada en su especificidad, lo que supone el mejor
enfoque desde un punto de vista de POO. La única restricción es situar en el método run las operaciones
costosas en tiempo.

Ejemplo de código de implementación de la interfaz Runnable:

package demorunnable;

public class MiClaseRunnable implements Runnable {
@Override
public void run() {

// Traza de inicio de la operación.
// Se utiliza el método de tipo static
// Thread.currentThread()
// para visualizar el nombre asignado a este thread
System.out.println("Inicio de una operación de 10 segundos "
+ "en el thread "
+ Thread.currentThread().getName());

// Aquí se simula un trabajo de 10 segundos (10 x 1000 ms)
for(int i=0; i<10; i++)
{
try {
// Utilización del método de tipo static sleep
// que espera el número de milisegundos que se debe
// esperar
Thread.sleep(1000);
}
catch (InterruptedException ex) {
// Aquí es posible insertar
// una operación que se llamará cuando
// el comando Thread.sleep se esté ejecutando
// y el Thread se interrumpa
}
// Traza testigo del trabajo en curso
System.out.println(i);
}
// Traza de final de operación
System.out.println("Fin de una operación de 10 segundos "
+ "en el thread "
+ Thread.currentThread().getName());
// Una vez que se llega a esta línea, el thread está "muerto"
// y no se puede volver a lanzar.
}

}

Instanciación y ejecución del thread:

package demorunnable;

public class Main {

public static void main(String[] args) {
System.out.println("Creación de un nuevo thread "
+ "desde el thread "
+ Thread.currentThread().getName());

// Utilización del constructor de Thread
// que recibe como argumento una referencia a un objeto
// que implementa la interfaz Runnable
Thread thread = new Thread(new MiClaseRunnable());

System.out.println("Inicio del nuevo thread");
thread.start();

System.out.println("Fin de main");

}
}

En los dos casos de uso, no hay nada que impida llamar a los métodos run «directamente». Como es
lógico, esto no tiene gran interés porque su ejecución utilizaría el thread que llama y no un nuevo thread.
La ejecución paralela solo se hará por medio de la utilización del método start.

3. Dormirse y esperar
Durante su ejecución, el thread puede pasar a esperar una «información exterior». Por ejemplo, espera la
recepción de un conjunto de bytes relativos a un periférico. Cuando el administrador de thread Java «da el
control» a la operación de recepción de franjas de bytes y esta se percata de que no hay bytes recibidos,
puede «devolver el control» de forma inmediata. De esta manera, no consume tiempo inútilmente y la
aplicación permanece reactiva.

Para esto, se utiliza el método de tipo static Thread.sleep del API Java.

Thread.sleep(<duración de hibernación en ms>)

Una llamada al método Thread.sleep(0) indica al administrador de Thread Java que la operación no tiene
nada que hacer por el momento y devuelve su tiempo de máquina (tiempo cuántico) al thread siguiente.

Una llamada al método Thread.sleep(1000) indica al administrador de Thread Java que la operación
actual debe esperar 1000 ms.

Durante su espera, el thread no consume ningún tiempo de máquina.

En la siguiente sección, veremos que es posible solicitar la parada de un thread en curso de ejecución. Si este
thread está «dormido» por un Thread.sleep y se pide que pare, entonces se va a despertar por medio de una
excepción de tipo InterruptedException.

Es la razón por la que IntelliJ IDEA se queja cuando el método se llama sin un try/catch o una cláusula throws
asociada.
// ...
try {
Thread.sleep(2000);
}
catch (InterruptedException ex) {
// Esta excepción se ha producido
// durante la pausa de 2 segundos.
// Alguien ha solicitado la parada del thread
// Por lo tanto, se van a "cerrar" los objetos
// y eliminar mi run lo más rápidamente posible.
// ...
return;
}
// ... continuación normal de las operaciones

Llamado a través de la referencia de un Thread, el método isAlive() devuelve false mientras que el thread
no esté arrancado y a continuación, true cuando se ejecuta el comando start.

Algunas veces sucede que un thread primario arranca un thread secundario, realiza una operación y, a
continuación, tiene que esperar el final de la ejecución de su thread secundario para pasar a la siguiente. Una
posible solución consiste en utilizar el método join llamado a través de la referencia del thread al que se
espera.

A continuación se muestra un ejemplo de utilización del método join.

El método join, como el método sleep, puede recibir una excepción de tipo InterruptedException durante su
fase de espera.

Código del lado del thread principal:

package demothreadjoin;

public class Main {

public static void main(String[] args) throws InterruptedException {

// Instanciación y a continuación lanzamiento
// de miThreadSecundario
System.out.println("Creación de un thread secundario "
+ "desde el thread principal llamado "
+ Thread.currentThread().getName());

// Utilización del constructor de Thread
// que recibe como argumento una referencia a un objeto
// que implementa la interfaz Runnable
Thread miThreadSecundario = new Thread(new MiClaseRunnable());

System.out.println("Arranque del thread secundario");
miThreadSecundario.start();


System.out.println("Continuación de las operaciones del
thread principal en paralelo con el del thread secundario");
// Trabajos sobre el thread principal
Thread.sleep(500);
// Necesidad de esperar al final de miThreadSecundario
// antes de continuar
System.out.println("Final de la operaciones del thread
principal; inicio de la espera al final del thread secundario");

try {
miThreadSecundario.join();
}
catch (InterruptedException ex) {
// Alguien ha solicitado la interrupción
// del thread principal. Se solicita
// la parada de miThreadSecundario y salir
// lo más rápido posible
}
// Continuación de la operación principal
// después del final de la operación secundaria.
System.out.println("Final de las operaciones del thread
secundario detectado, continuación de las operaciones del thread
principal");
Thread.sleep(500);
System.out.println("Final de las operaciones del thread
principal");
}
}

Código del lado del thread secundario:

package demothreadjoin;

public class MiClaseRunnable implements Runnable {
@Override
public void run() {
System.out.println("Inicio operaciones del nuevo thread llamado "
+ Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Fin operaciones de "
+ Thread.currentThread().getName());
}
}

4. Abandono desde el thread primario


Una vez lanzado, es muy arriesgado «matar» un thread. No obstante, existe un método stop() en la clase
Thread, pero está obsoleto, es decir, que va a desaparecer del API Java en cualquier momento. En efecto,
«matar» un thread implica eliminar todas las protecciones de acceso a los objetos que manipula para dejarlos
en estados transitorios al resto de los threads. En resumen, a continuación se producirán resultados totalmente
aleatorios.

La mejor manera de detener un thread en Java es colaborativa. Consiste en compartir entre thread principal y
thread secundario una variable (bandera) que el primero va a modificar cuando se desee la parada y que el
segundo comprobará lo más a menudo posible durante su ejecución.

Tan pronto como la bandera indique la «parada inmediata», el thread finalizará el flujo de instrucciones que
ejecutaba después de haber liberado los objetos utilizados. Este principio ya está previsto en el API Java
gracias a dos métodos Thread.interrupt() (llamado por el thread principal) y Thread.isInterrupted() (llamado
periódicamente por el thread que se va a detener).

La buena práctica cuando se implementa un thread es insertar lo más habitualmente posible en su código
llamadas a Thread.isInterrupted() para saber si se debe continuar su ejecución o no.

Veamos esto con un ejemplo muy sencillo, en el que el thread principal -por lo tanto, el main- crea un thread
secundario que muestra constantemente las series de cifras del 0 al 9. Después de tres segundos, el thread
principal decide terminar la actividad del thread secundario y, a continuación, espera a que el thread
secundario haya terminado realmente.

Código del lado del thread principal:

package demothreadcancel;

public class Main {

public static void main(String[] args)
throws InterruptedException {

System.out.println("Instanciación "
+ "del thread secundario");
Thread miThreadSecundario
= new Thread(new MiClaseRunnable());

System.out.println("Arranque "
+ "del thread secundario");
miThreadSecundario.start();

// Simula un trabajo de 3 segundos en el thread principal
Thread.sleep(3000);

System.out.println("Solicita "
+ "la parada del thread secundario");
miThreadSecundario.interrupt();

System.out.println("Espera "
+ "la ’muerte’ del thread secundario");
miThreadSecundario.join();

System.out.println("Fin de main");
}
}

Código del lado del thread secundario:

package demothreadcancel;

public class MiClaseRunnable implements Runnable {
@Override
public void run() {
// Traza de inicio de la operación.
// Se utiliza el método de tipo static
// Thread.currentThread()
// para visualizar el nombre asignado a este thread
System.out.println("Inicio del thread secundario");

// Aquí se ejecuta el bucle mientras nadie
// solicite parar
int i=0;
while( true ){
// Visualización continua de 0 a 9
System.out.println(i);
i = i+1;
if( i== 10 )
i=0;

if( Thread.currentThread().isInterrupted()
== true ){
System.out.println(
"El thread secundario ha detectado "
+ "una petición de interrupción");
// Fuerza la salida del bucle
break;
}
}
// Traza el final de la operación
System.out.println("Fin del thread secundario");
// Una vez pasada esta línea el thread está "muerto"
}

}

Salida por la consola:

(.)
6
7
8
9
0
1
2
3
Solicita la parada del thread secundario
Espera la ’muerte’ del thread secundario
El thread secundario ha detectado una petición de interrupción
Fin del thread secundario
Fin de main

Puede ser que entre el momento en el que el thread principal solicita la interrupción y que esta petición se
tenga en cuenta realmente por el thread secundario se puedan mostrar varias cifras.

5. Threads y clases anónimas


Si la clase que aloja la operación a «threader» solo contiene el método run, puede simplificar
significativamente su sintaxis de llamada corriendo el riesgo de perder algo de claridad en su código.

a. Con la interfaz Runnable

Para esto se va a instanciar un objeto de tipo Runnable «sobre la marcha» y su referencia se va a pasar
inmediatamente al constructor del objeto Thread. La clase de tipo Runnable correspondiente se llama
anónima porque no tiene nombre.

Sintaxis

Thread <nombre> = new Thread(


new Runnable()
{
@Override
public void Run(){
<el código que se ha de threader>}
});

Ejemplo

Thread miThread =
new Thread(new Runnable(){
@Override
public void run() {
System.out.println("Thread secundario");
}
});
miThread.start();
Si no necesita referencia del thread creado (miThread en el ejemplo anterior), entonces puede simplificar su
código todavía más.

Ejemplo

new Thread(new Runnable(){


@Override
public void run() {
System.out.println("Thread secundario");
}
}).start();

Naturalmente, no teniendo referencia del thread que está funcionando, ya no puede direccionarlo
directamente para, por ejemplo, abandonarlo usando el sistema clásico.

b. Con la clase Thread

En el ejemplo anterior, hemos instanciado un objeto de tipo Runnable en una lógica de implementación de
interfaz. También es posible extender la clase Thread, redefiniendo su método run «sobre la marcha».

Sintaxis

Thread <nombre> = new Thread(


@Override
public void Run(){
<el código que se ha de threader>}
);

Ejemplo

Thread miThread =
new Thread(){
@Override
public void run() {
System.out.println("Thread secundario");
}
};
miThread.start();

Misma posibilidad que antes: si no necesita una referencia del thread instanciado, puede encadenar la
llamada al método start.

new Thread(){
@Override
public void run() {
System.out.println("Thread secundario");
}
}.start();

c. Acceso simplificado a las variables y datos miembro

Esta sintaxis le puede parecer desconcertante y determinados desarrolladores ni siquiera quieren oír hablar
de ella. Sin embargo, se ha extendido y presenta algunas ventajas que pueden hacer que el código se
simplifique y, con él, la vida del desarrollador.

El método run de una clase anónima tiene acceso a las variables locales de tipo «final» del método que
lo instancia y arranca el thread, y todo esto durante toda su ejecución.

El método run de una clase anónima tiene acceso a todos los datos miembros de la clase que aloja el
thread.

Veamos esto de forma más detallada con este ejemplo:

package demothreadanonimo;

public class MiClase {

// Como el thread debe poder detenerse "desde el exterior"
// MiClase memoriza su referencia
private Thread miTratamiento = null;

// Un dato miembro privado para permitir el acceso
// independientemente del nivel de acceso
private String miCampo
= "Contenido de un campo de mi clase";

// Un contador que se incrementará en el thread
protected int numeroDeDias = 0;

// Un getter de este contador
public int getNumeroDeDias() {
return numeroDeDias;
}

// Método de inicio de las operaciones
// que recibe una tabla de String como argumento
// Como esta tabla:
// - forma parte de las variables locales
// del método ExecuteTratamiento
// - se utiliza durante la ejecución del thread
// entonces debe ser de tipo final
public void ExecuteTratamiento(final String[] argumentos){

// Este entero local al método pero utilizado
// por el thread también debe ser de tipo final
final int coef = 1;

// Instanciación del thread
// y de la clase anónima de tipo Runnable
miTratamiento = new Thread(
new Runnable(){

// Codificación del método run de la interfaz
@Override
public void run() {

System.out.println("Inicio de la operación");

// Bucle principal ejecutado mientras
// nadie haya pedido la parada
while(Thread.currentThread().isInterrupted()
== false ){
System.out.println("Tengo acceso "
+ "a miCampo que contiene: "
+ miCampo);
System.out.println("Sigo teniendo "
+ "acceso a los argumentos "
+ "pasados a mi ejecución");
for(String argumento: argumentos){
System.out.println(argumento);
}
// Puedo modificar los datos
// miembro de la clase
numeroDeDias+=coef;
}
System.out.println("Fin de la operación");
}
}
);
// Lanzamiento del thread
miTratamiento.start();
// y retorno del método.
// En teoría los datos locales
// realmente desaparecen
// pero Java los mantiene porque sabe
// que el thread se está ejecutando
// y quién los utiliza
}


// Parada de la operación con petición de interrupción del thread
public void ParadaTratamiento(){
System.out.println("Solicitud de parada de la operación");
miTratamiento.interrupt();
try {
miTratamiento.join();
}
catch (InterruptedException ex) {
}
}
}

Del lado de main:

package demothreadanonimo;

public class DemoThreadAnonimo {

public static void main(String[] args) throws
InterruptedException {
System.out.println("Instanciación de MiClase");
MiClase miClase = new MiClase();
System.out.println("Ejecución de la operación");
miClase.ExecuteTratamiento(
new String[]{"Argumento1", " Argumento2", " Argumento3"});

Thread.sleep(1000);

System.out.println("Fin de la operación");
miClase.ParadaTratamiento();
System.out.println("Número de vueltas: "
+ miClase.getNumeroDeVueltas());
}

}

A continuación se muestra el resultado:


Sincronización entre threads

1. Necesidad de la sincronización
La programación de varias rutas de ejecución no plantea ningún problema particular hasta que comparten la
misma información o los mismos recursos. En efecto, dando por hecho que el sistema operativo puede
interrumpir las operaciones en cualquier momento, se corre el riesgo de tener objetos que estén modificando
un thread preferente, que se encuentre en estados inestables para el thread siguiente. Para protegerse de
estos funcionamientos incorrectos, hay que «sincronizar» los threads, es decir, proteger las zonas delicadas de
las operaciones.

Esto no se va a reproducir en el sistema de gestión, que continuará activando los threads unos después de
otros; sencillamente cuando un thread A necesite acceder a un dato común protegido que un thread B no haya
terminado de actualizar, entonces el thread A deberá «esperar a la siguiente vuelta». Y si el trabajo del thread
B no ha terminado en una vuelta, entonces tendrá que esperar a la siguiente y así sucesivamente.

El mismo principio se aplica si se trata de una operación común que el thread B deberá haber terminado antes
de que el thread A pueda realizarlo a su vez. Este es el escenario que ofrece el siguiente extracto de código. En
efecto, la operación permite visualizar una cuenta desde cero hasta nueve, realizada por diez threads. El
objetivo es obtener la siguiente visualización:

0123456789
0123456789
0123456789
0123456789
0123456789
0123456789
0123456789
0123456789
0123456789
0123456789

A continuación se muestra una primera versión de código «sin protección»:

package demothreadsinsincro;


public class TratamientoSinProteccion{

public void EjecutarTratamiento() {

for(int i=0; i<10; i++){
new Thread(new Runnable()
{
@Override
public void run() {
for (int j = 0; j < 10; j++) {
System.out.print(j);
}
System.out.println();
}
}).start();
}
}
}

package demothreadsinsincro;

public class DemoSincroThread {

public static void main(String[] args) {

TratamientoSinProteccion tsp
= new TratamientoSinProteccion();
tsp.EjecutarTratamiento();
}

}

Nada más que lo que ya se ha presentado: un bucle de lanzamiento de diez threads, donde cada uno muestra
una cuenta que va desde cero hasta nueve.

A continuación se muestra la salida correspondiente en pantalla:


¡Menudo desorden!

2. Los métodos «syncronized»


La primera solución es de una simplicidad casi infantil. Consiste en agregar una palabra clave en la declaración
de los métodos que se deben proteger. Esta palabra clave será traducida por la máquina virtual en un contexto
de ejecución particular, que se va a encargar de proteger todos los métodos de la clase de ejecuciones
concurrentes, identificados como syncronized.

A continuación se muestra el código correspondiente:

package demothreadconsyncronized;


public class TratamientoConProteccionPorSyncronized{

public void EjecutarTratamiento() {

for(int i=0; i<10; i++){
new Thread(new Runnable()
{
@Override
public void run() {
Mostrar();
}
}).start();
}
}

private syncronized void Mostrar(){
for (int j = 0; j < 10; j++) {
System.out.print(j);
try {
Thread.sleep(0);
}
catch (InterruptedException ex) {
return;
}
}
System.out.println();
}
}

package demothreadconsyncronized;

public class Main {

public static void main(String[] args) {
TratamientoConProteccionPorSyncronized tsp
= new TratamientoConProteccionPorSyncronized();
tsp.EjecutarTratamiento();
}
}

Y a continuación se muestra la salida por la consola correspondiente:


Mejor así, sin duda.

Puede observar que con el tipo syncronized, que no tiene ningún efecto sobre el método run, las operaciones
se han transportado a un método Mostrar -llamado desde el run- y que puede adoptar la indicación
syncronized.

Este primer método es muy eficaz, pero un poco radical. En efecto, ninguno de los métodos de una clase
definidos con la palabra clave syncronized se podrá ejecutar nunca más en paralelo, lo que no es óptimo.

Los constructores no pueden ser syncronized. Esto no tendría ningún sentido porque varios threads no
pueden compartir la instanciación de un mismo objeto.

3. Las operaciones «syncronized»


La idea de esta segunda solución es tomar un objeto y utilizarlo como «bloqueo» para limitar el acceso a la
porción sensible del código. Esta porción sensible también se llama sección crítica. El objeto «bloqueo»
generalmente es el propio objeto cuando queremos proteger una de sus propiedades. En otro caso, un objeto
de tipo Object es perfectamente adecuado.

A continuación, la palabra clave syncronized y sus paréntesis entran en escena para delimitar la zona que hay
que proteger.

He aquí de nuevo nuestro código, esta vez bloqueado:

package demothreadconobjetosyncronized;


public class TratamientoConProteccion2 {
private Object bloqueo = new Object();
public void EjecutarTratamiento() {

for(int i=0; i<10; i++){
new Thread(new Runnable()
{
@Override
public void run() {
syncronized(bloqueo) {
for (int j = 0; j < 10; j++) {
System.out.print(j);
try {
Thread.sleep(0);
}
catch (InterruptedException ex) {
return;
}
}
System.out.println();
}
}
}).start();
}
}


}

package demothreadconobjetosyncronized;

public class Main {

public static void main(String[] args) {
TratamientoConProteccionPorObjetoSyncronized tsp
= new TratamientoConProteccionPorObjetoSyncronized();
tsp.EjecutarTratamiento();

}
}

Su salida por la consola correspondiente es la esperada.

Una vez que un thread tiene el control de un bloqueo, este se convierte en inaccesible para el resto.

Una vez que un thread adquiere un bloqueo, lo es hasta el final de la operación. Si esta operación llama a
métodos que están protegidos a su vez por el mismo bloqueo, entonces la fase de adquisición no se
vuelve a lanzar.

4. La clase Semaphore
Imagine la sala de un gimnasio, «reformada» con diez aparatos de musculación muy «eficaces». Los usuarios
de la sala son más numerosos que los aparatos y se forma una cola de espera después de que empiecen los
diez primeros usuarios. Cuando un usuario se cansa, deja su puesto de entrenamiento y otro le sustituye. De
esta manera, la cola de espera se vacía progresivamente.

Si este caso de uso se debiera programar, sería particularmente oportuno utilizar un objeto de tipo Semaphore.
Un Semaphore es una especie de bloqueo que se cierra después de un determinado número de pasadas y se
vuelve a abrir cuando se libera un poco de espacio. Este bloqueo protege una región crítica, como la utilización
de los diez aparatos de nuestro ejemplo.

Un Semaphore que solo soporta una única instancia de utilización se llama Mutex.

El API Java ofrece una implementación de Semaphore con su clase java.util.concurrent.Semaphore.

Cuando se instancia un objeto de tipo Semaphore, el desarrollador le pasa el número máximo de instancias
que puede adquirir.

package demosemaforo;

public class DemoSemaforo {

public static void main(String[] args) {

// En primer lugar, definición de la sala
// de entrenamiento que contiene cuatro puestos
final SalaMusculacion salaMusculacion
= new SalaMusculacion();

// Seis deportistas se presentan para entrenarse
for(int i=0; i<6; i++){

// Para trazar lo que va a pasar,
// les damos un nombre
final String NumeroDeportistas
= "Deportista n."+(i+1);

// El thread va a esperar un lugar libre
// y a continuación desarrollar el entrenamiento
// de nuestra deportista
new Thread(new Runnable(){
@Override
public void run() {
try {
salaMusculacion.Training(NumeroDeportistas);
}
catch (InterruptedException ex) {
}
}
}).start();
}
}

}

package demosemaforo;

import java.util.concurrent.Semaphore;

public class SalaMusculacion {

private final Semaphore permiso = new Semaphore(4);

// Un deportista llama a este método
// para acceder a un puesto de entrenamiento
public void Training(String deportista) throws
InterruptedException {
try {
System.out.println(deportista +" quiere entrenarse");
NumeroPuestosLibres();
// Puede quedar bloqueado en este punto
// mientras no haya ningún puesto libre.
permiso.adquirir();

// Ahora puede entrenarse
System.out.println("Inicio del entrenamiento del Deportista número: "+deportista);
Thread.sleep(1000);
System.out.println("Fin del entrenamiento para Deportista número "+Deportista);
}

finally {
// Después de una buena sesión
// nuestro deportista libera su puesto
permiso.libera ();
NumeroPuestosLibres();
}
}

public void NumeroPuestosLibres(){
System.out.println("Quedan "
+permiso.disponible()
+ " puesto(s) libre(s)");
}
}

A continuación se muestra el resultado:


Comunicación interthreads

1. El método join
Como se ha visto anteriormente, el método join permite a un thread principal «hibernar» mientras espera el
final de la ejecución de un thread secundario.

Ejemplo de código

using System;
using System.Threading;

namespace SincroInterThreads
{
class Program
{
static void Main(string[] args)
{
Prueba t = new Prueba();
t.TratamientoPrincipal();
}
}

class Prueba
{
public void TratamientoPrincipal()
{
Console.WriteLine("Inicio TratamientoPrincipal");
ThreadStart ts
= new ThreadStart(TratamientoSecundario);
Thread t = new Thread(ts);
t.IsBackground = false;
t.Priority = ThreadPriority.Highest;
t.Name = "Es mi thread:)";
t.Start();


t.Join();

Console.WriteLine("Fin TratamientoPrincipal");
}

private void TratamientoSecundario()
{
Console.WriteLine("Inicio TratamientoSecundario");
Thread.Sleep(1000 * 10);
Console.WriteLine("Fin TratamientoSecundario");
}

}
}

Salida por la consola asociada:

Inicio TratamientoPrincipal
Inicio TratamientoSecundario
Fin TratamientoSecundario
Fin TratamientoPrincipal
Pulse una tecla para continuar.

Salida por la consola asociada sin la línea t.Join();:

Inicio TratamientoPrincipal
Fin TratamientoPrincipal
Inicio TratamientoSecundario
Fin TratamientoSecundario
Pulse una tecla para continuar.

Un thread hibernado o bloqueado no consume tiempo de máquina.

La utilización del método join es muy eficaz cuando queremos sincronizarnos con el final de una operación
completa.

2. Los objetos de sincronización


Si queremos sincronizarnos con determinadas fases de una operación secundaria, el método join no se puede
utilizar. Por ejemplo, imaginemos un thread A encargado de recuperar las franjas de bytes y un thread B
encargado tratarlos. El thread A no se va a detener después de la primera franja recibida; va a continuar
recibiendo otros bytes y a prepararlos para formar la siguiente franja. Por lo tanto, el thread B no puede utilizar
un join porque el thread A no tiene final calculado. Ya no estamos en una lógica orientada a eventos que un
esquema Observador/Observado clásico podría resolver directamente. Aquí el thread B está «dormido» y se
debe despertar cuando el thread A tenga una franja lista para ser consumida.

El objeto de sincronización que se ha de utilizar entre el thread A y el thread B es cualquiera a partir del
momento en el que hereda de la clase java.lang.Object. En nuestro ejemplo, utilizaremos el objeto
GeneradorDeFranjas.

java.lang.Object.notify

Un thread productor envía una señal hacia uno o varios threads consumidores cuando le parece oportuno, a
través de los métodos java.lang.Object.notify -para un consumidor a la vez- y java.lang.Object.notifyAll -para
todos los consumidores guardados-.

Si nadie «está a la escucha», entonces esta señal se pierde.

java.lang.Object.wait

Por el contrario, un thread espera esta señal habiendo llamado al método java.lang.Object.wait. Entonces, el
administrador de threads de la máquina virtual sabrá encontrarlo y despertarlo.

Las llamadas a los métodos notify y wait se deben hacer en métodos syncronized.

Volvamos a nuestro ejemplo:

threadProductor y threadConsumidor comparten un mismo objeto de sincronización, a saber,


threadProductor, que cumplirá una doble función.

threadConsumidor, que no tiene nada que tratar inicialmente, llamará al método wait del objeto
threadProductor. Esta llamada provoca la hibernación de threadConsumidor.

Después de recibir una franja completa, threadProductor la pone en la cola de espera y, a continuación,
llama a «su» método notify().

Entonces, el administrador del thread de la máquina virtual despierta a threadConsumidor, que accede
a la cola de espera de las franjas formateadas para recuperar la nueva franja recibida. Durante este
tiempo, threadProductor recibe y construye la siguiente franja.

Tan pronto como la franja se consume, threadConsumidor vuelve a hibernar, llamando al método wait
del objeto threadProductor.

A continuación se muestra un ejemplo de codificación de nuestra sincronización productor/consumidor. La


franja es un sencillo número aleatorio.

package demosincrointerthreads;

import java.io.IOException;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;

// La clase que permite instanciar la demostración
public class DemoSincroInterThreads {

public static void main(String[] args) {
ProductorConsumidor productorConsumidor
= new ProductorConsumidor();
productorConsumidor.Acción();
}
}

// La clase ProductorConsumidor que instancia
// e inicia los threads.
// A continuación espera una acción de teclado
// para detener el conjunto.
class ProductorConsumidor {

public void Accion() {

System.out.println("Inicio Tratamiento global");

// Instanciación del thread productor
GeneradorDeFranjas miReceptorDeFranjas
= new GeneradorDeFranjas();
Thread threadProductor
= new Thread(miReceptorDeFranjas);
// Inicio del thread productor
threadProductor.start();

// Instanciación del thread consumidor
Thread threadConsumidor
= new Thread(
new ConsumidorDeFranjas(miReceptorDeFranjas));
// Inicio del thread consumidor
threadConsumidor.start();

// Los dos threads actúan...
System.out.println(
"Pulse una tecla para parar. ");
try {
// hasta que se pulsa una tecla
System.in.read();
}
catch (IOException ex) {}

// En este momento, se solicita la interrupción
// de los dos threads
System.out.println("Abandono solicitado ");
threadProductor.interrupt();
threadConsumidor.interrupt();
try {
// A continuación, esperamos que terminen
threadProductor.join();
threadConsumidor.join();
} catch (InterruptedException ex) {}

System.out.println("Final Tratamiento global");

}
}

// La clase generadora de franjas
class GeneradorDeFranjas implements java.lang.Runnable {

// El objeto de almacenamiento de las "tramas"
// Aquí se utiliza una Queue
// para un funcionamiento de tipo FIFO
private Queue<Integer> fileTrames
= new LinkedList<Integer>();

private ConsumidorDeFranjas miConsumidorDeFranjas;

public void setMiConsumidorDeFranjas(
ConsumidorDeFranjas miConsumidorDeFranjas) {
this.miConsumidorDeFranjas = miConsumidorDeFranjas;
}

// El método principal
@Override
public void run() {

System.out.println("Inicio GeneradorDeFranjas");

// Mientras que "nadie se pare ".
while (Thread.currentThread().isInterrupted()
== false ) {
// Simulación de una recepción de franja
try {
Thread.sleep(100);
}
catch (InterruptedException ex) {
System.out.println(
"InterruptedException "
+ "en GeneradorDeFranjas");
break;
}
int numeroFranja = DameUnEnteroAleatorio();
System.out.println("Franja recibida: " + numeroFranja);

// Encola la franja recibida
fileTrames.add(numeroFranja);

// Señala al threadConsumidor
// que puede leer la franja
avisarFranjaRecibida();
try {
esperaArchivoVacio();
} catch (InterruptedException ex) {
System.out.println(
"InterruptedException "
+ "en esperaArchivoVacio");
break;
}

}
System.out.println("Final de GeneradorDeFranjas");
}

// Generador aleatorio de números enteros
Random rand = new Random();
private int DameUnEnteroAleatorio() {

int randomNum = rand.nextInt(101);
return randomNum;
}

// Este método interviene en notify
// y por lo tanto debe ser de tipo syncronized
private syncronized void avisarFranjaRecibida(){
notify();
}


private void esperaArchivoVacio()
throws InterruptedException{

if( fileTrames.size() >= 10 ){
System.out.println("Cola saturada ");
while(fileTrames.size() > 0){
this.miConsumidorDeFranjas
.esperaConsumo();
}
System.out.println("Cola despejada ");
}
}

// Este método de tipo public permite desapilar
// las franjas recibidas. Como utiliza wait,
// debe ser de tipo syncronized
public syncronized int leeFranjaRecibida()
throws InterruptedException{

// Mientras haya franjas por leer,
// las leemos
if( fileTrames.size() > 0)
return fileTrames.poll();

// En caso contrario, esperamos a la siguiente
wait(); // La llamada del wait libera el bloqueo
// del syncronized para permitir
// la llamada a avisarFranjaRecibida()
// En la salida del wait
// el thread retoma el bloqueo.
return fileTrames.poll();
}
}

// La clase consumidora de franjas
class ConsumidorDeFranjas implements java.lang.Runnable {

// Una referencia al productor permite
// un acceso a su método de lectura
private GeneradorDeFranjas miReceptorDeFranjas;

// El constructor sobrecargado permite
// informar al productor
public ConsumidorDeFranjas(
GeneradorDeFranjas miReceptorDeFranjas){
this.miReceptorDeFranjas
= miReceptorDeFranjas;
this.miReceptorDeFranjas
.setMiConsumidorDeFranjas(this);
}


@Override
public void run() {
System.out.println("Inicio ConsumidorDeFranjas");

// Mientras que "nadie se pare ".
while (Thread.currentThread().isInterrupted()
== false )
{
try {
// Llamada a un método bloqueante
// del productor
System.out.println("Consumidor espera");
int numeroFranja
= miReceptorDeFranjas.leeFranjaRecibida();

// Tratamiento de la franja recibida
System.out.println("Franja consumida: "
+ numeroFranja);

Thread.sleep(500);

franjaConsumida();

} catch (InterruptedException ex) {
System.out.println(
"InterruptedException "
+ “en ConsumidorDeFranjas");
break;
}
}
System.out.println("Fin ConsumidorDeFranjas");
}

private syncronized void franjaConsumida() {
notify();
}

public syncronized void esperaConsumo()
throws InterruptedException{
wait();
}

}

Salida por la consola correspondiente:

debug:
Inicio Tratamiento global
Inicio GeneradorDeFranjas
Pulse una tecla para parar.
Inicio ConsumidorDeFranjas
Franja recibida: 31
Franja consumida: 31
Franja recibida: 53
Franja consumida: 53
.
Franja recibida: 8
Franja consumida: 8
Franja recibida: 0
Franja consumida: 0
Franja recibida: 76
Franja consumida: 76

Abandono solicitado
InterruptedException en GeneradorDeFranjas
Final de GeneradorDeFranjas
InterruptedException en ConsumidorDeFranjas
Fin ConsumidorDeFranjas
Final Tratamiento global
BUILD SUCCESSFUL (total time: 5 seconds)

Durante la ejecución de este programa, controle que el foco esté sobre la ventana Ouput de IntelliJ IDEA antes
de pulsar en una tecla y, a continuación, [Intro] para abandonar.

La salida por la consola muestra una perfecta cadencia entre franja producida y franja consumida. Es normal,
la producción es más lenta que el consumo.

Si por el contrario la operación de consumo lleva un poco más de tiempo que la de producción, las franjas se
van a apilar más rápido que el tiempo que se tarda en desapilarlas.

Se soluciona este desequilibrio introduciendo una «pausa» en el bucle de consumo:

while (Thread.currentThread().isInterrupted()
== false )
{
try {
// Llamada a un método bloqueante
// del productor
int numeroFranja
= miReceptorDeFranjas.leeFranjaRecibida();

// Tratamiento de la franja recibida
System.out.println("Franja consumida: "
+ numeroFranja);

Thread.sleep(500);

} catch (InterruptedException ex) {
System.out.println(
"InterruptedException "
+ "en ConsumidorDeFranjas");
break;
}
}

A continuación se muestra la salida por la consola:

debug:
Inicio Tratamiento global
Inicio GeneradorDeFranjas
Pulse una tecla para parar.
Inicio ConsumidorDeFranjas
Franja recibida: 0
Franja consumida: 0
Franja recibida: 11
Franja recibida: 73
Franja recibida: 23
Franja recibida: 66
Franja recibida: 4
Franja recibida: 100
Franja consumida: 11
Franja recibida: 7
Franja recibida: 48
Franja recibida: 98
Franja recibida: 84
Franja recibida: 30
Franja recibida: 84

Abandono solicitado
InterruptedException en ConsumidorDeFranjas

Fin ConsumidorDeFranjas

InterruptedException en GeneradorDeFranjas
Final de GeneradorDeFranjas
Final Tratamiento global
BUILD SUCCESSFUL (total time: 5 seconds)

Al cabo de un momento la cola estará llena y se producirá una excepción.

El ejercicio que vamos a proponer en la siguiente sección consiste en completar el ejemplo para que la
producción se detenga cuando se alcance un determinado número de franjas (diez, por ejemplo) en la cola de
espera, y se retome cuando se vacíe.
Ejercicio

1. Enunciado
Partiendo del ejemplo anterior (con Thread.sleep(500); en el bucle de consumo), debe introducir la noción de
gestión de flujo entre productor y consumidor. El thread de producción debe «dormir» cuando se alcance un
número máximo de franjas en espera (diez por ejemplo). El thread de consumo leerá estas franjas y después,
cuando la cola esté vacía, el thread de producción deberá retomar su trabajo.

Tipo de comportamiento deseado:

debug:
Inicio Tratamiento global
Inicio GeneradorDeFranjas
Pulse una tecla para parar.
Inicio ConsumidorDeFranjas
Consumidor espera
Franja recibida: 46
Franja consumida: 46
Franja recibida: 67
Franja recibida: 23
Franja recibida: 92
Franja recibida: 20
Franja recibida: 23
Franja consumida: 67
Franja recibida: 54
Franja recibida: 21
Franja recibida: 60
Franja recibida: 66
Franja consumida: 23
Franja recibida: 2
Franja recibida: 58

Franja recibida: 69

Fila saturada
Franja consumida: 92
Franja consumida: 20
Franja consumida: 23
Franja consumida: 54
Franja consumida: 21
Franja consumida: 60
Franja consumida: 66
Franja consumida: 2
Franja consumida: 58
Franja consumida: 69
Saturación de la cola eliminada
Franja recibida: 97
Franja recibida: 76
Franja recibida: 41
Franja recibida: 89
Franja consumida: 97
Franja recibida: 12

Abandono solicitado
InterruptedException en GeneradorDeFranjas
Final de GeneradorDeFranjas
InterruptedException en ConsumidorDeFranjas
Fin ConsumidorDeFranjas
Final Tratamiento global
BUILD SUCCESSFUL (total time: 9 seconds)

A continuación se muestra alguna información para ayudarle:

En el código inicial, el desarrollador ha generado una doble función para el productor: producir y sincronizar el
consumidor para que duerma cuando no haya nada que leer. Una solución posible para la nueva problemática
sería hacer lo mismo con el consumidor y permitir que el productor se duerma cuando haya leído todo.

2. Corrección
Cree un mecanismo que permita al consumidor referenciar el productor para que este último pueda llamar
a sus métodos de sincronización.

Añada al consumidor dos métodos de tipo syncronized; uno que el consumidor deberá llamar cuando haya
consumido una franja, y el segundo, que el productor deberá llamar cuando la cola esté saturada y espere
a que se vacíe.

Adapte el thread del consumidor para que llame al método franja consumida.

Adapte el thread del productor para que, después de encolar una franja, compruebe si la cola está
saturada. Llegado el caso, utilizará el método de sincronización del consumidor para retomar sus
operaciones.

package labsincrointerthreads;

import java.io.IOException;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;

// La clase que permite instanciar la demostración
public class LabSincroInterThreads {


public static void main(String[] args) {
ProductorConsumidor productorConsumidor
= new ProductorConsumidor();
productorConsumidor.Acción();
}
}

// La clase ProductorConsumidor que instancia
// y arranca los threads.
// A continuación espera una acción de teclado
// para detener el conjunto.
class ProductorConsumidor {

public void Accion() {

System.out.println("Inicio Tratamiento global");

// Instanciación del thread productor
GeneradorDeFranjas miReceptorDeFranjas
= new GeneradorDeFranjas();
Thread threadProductor
= new Thread(miReceptorDeFranjas);
// Inicio del thread productor
threadProductor.start();

// Instanciación del thread consumidor
Thread threadConsumidor
= new Thread(
new ConsumidorDeFranjas(miReceptorDeFranjas));
// Inicio del thread consumidor
threadConsumidor.start();

// Los dos threads se ejecutan.
System.out.println(
"Pulse una tecla para parar.");
try {
// ... hasta que se pulse una tecla
System.in.read();
}
catch (IOException ex) {}

// En este momento, solicitamos la interrupción
// de los dos threads
System.out.println("Abandono solicitado");
threadProductor.interrupt();
threadConsumidor.interrupt();
try {
// A continuación, esperamos que se terminen
threadProductor.join();
threadConsumidor.join();
} catch (InterruptedException ex) {}

System.out.println("Final Tratamiento global");

}
}

// La clase generadora de franjas
class GeneradorDeFranjas implements java.lang.Runnable {

// El objeto de almacenamiento de las "franjas"
// Aquí es una Queue que se utiliza
// para un funcionamiento de tipo FIFO
private Queue<Integer> archivoFranjas
= new LinkedList<Integer>();
// Todo lo necesario para que el consumidor se guarde
private ConsumidorDeFranjas miConsumidorDeFranjas;

public void setMiConsumidorDeFranjas(
ConsumidorDeFranjas miConsumidorDeFranjas) {
this.miConsumidorDeFranjas = miConsumidorDeFranjas;
}

// El método principal
@Override
public void run() {

System.out.println("Inicio GeneradorDeFranjas");

// Mientras que "nadie se detenga ".
while (Thread.currentThread().isInterrupted()
== false ) {

// Simulación de una recepción de franja
try {
Thread.sleep(100);
}
catch (InterruptedException ex) {
System.out.println(
"InterruptedException "
+ "en GeneradorDeFranjas");
break;
}
int numeroFranja = DameUnEnteroAleatorio();
System.out.println("Franja recibida: " + numeroFranja);

// Encolado de la franja recibida
fileTrames.add(numeroFranja);

// Indica al threadConsumidor
// que puede leer la franja
avisarFranjaRecibida();

// Si la cola está llena
// entonces hay que esperar a se vacíe
try {
esperaArchivoVacio();
} catch (InterruptedException ex) {
System.out.println(
"InterruptedException "
+ "en esperaArchivoVacio");
break;
}

}
System.out.println("Final de GeneradorDeFranjas");
}

// Generator aleatorio de números enteros
Random rand = new Random();
private int DameUnEnteroAleatorio() {

int randomNum = rand.nextInt(101);
return randomNum;
}

// Este método interviene sobre notify
// y por lo tanto debe ser de tipo syncronized
private syncronized void avisarFranjaRecibida(){
notify();
}

// Este método se llama cuando se ha encolado
// una nueva franja.
// Permite probar si la cola está saturada
// y, si es el caso, esperar a que se vacíe.
private void esperaArchivoVacio()
throws InterruptedException{

if( archivoFranjas.size() >= 10 ){
System.out.println("Cola saturada");
while(fileTrames.size() > 0){
this.miConsumidorDeFranjas
.esperaConsumo();
}
System.out.println("Saturación de la cola eliminada");
}
}

// Este método de tipo public permite desapilar
// las franjas recibidas. Como utiliza wait
// debe ser de tipo syncronized
public syncronized int leeFranjaRecibida()
throws InterruptedException{

// Si hay franjas que leer,
// se leen
if( archivoFranjas.size() > 0)
return archivoFranjas.poll();

// En caso contrario, esperamos a la siguiente
System.out.println("Consumidor en espera");
wait(); // La llamada del wait libera el bloqueo
// del syncronized para permitir la llamada
// a avisarFranjaRecibida()
// En la salida del wait
// el thread retoma el bloqueo.
return archivoFranjas.poll();
}
}

// La clase consumidora de franjas
class ConsumidorDeFranjas implements java.lang.Runnable {

// Una referencia al productor permite
// un acceso a su método de lectura
private GeneradorDeFranjas miReceptorDeFranjas;

// El constructor sobrecargado permite
// indicar el productor
public ConsumidorDeFranjas(
GeneradorDeFranjas miReceptorDeFranjas){
this.miReceptorDeFranjas
= miReceptorDeFranjas;
this.miReceptorDeFranjas
.setMiConsumidorDeFranjas(this);
}


@Override
public void run() {
System.out.println("Inicio ConsumidorDeFranjas");

// Mientras que "nadie se detenga".
while (Thread.currentThread().isInterrupted()
== false )
{
try {
// Llamada a un método bloqueante
// del productor

int numeroFranja

= miReceptorDeFranjas.leeFranjaRecibida();

// Tratamiento de la franja recibida
System.out.println("Franja consumida: "
+ numeroFranja);

Thread.sleep(500);

franjaConsumida();

} catch (InterruptedException ex) {
System.out.println(
"InterruptedException "
+ "en ConsumidorDeFranjas");
break;
}
}
System.out.println("Fin ConsumidorDeFranjas");
}

private syncronized void franjaConsumida() {
notify();
}

public syncronized void esperaConsumo()
throws InterruptedException{
wait();
}
}
9. LAS PRUEBAS
Introducción
Cuando un cliente pide un desarrollo, hay unas especificaciones que describen las funcionalidades de nivel
superior en forma de «casos concretos de uso». Este documento servirá más tarde para validar los desarrollos
realizados. Estos casos de uso van a poner en acción numerosos objetos desarrollados por el equipo. Estos
objetos se van a comunicar entre ellos con los métodos, siguiendo una cronología cuidadosamente pensada
durante el análisis. Cada intercambio se realizará la mayor parte del tiempo con argumentos de «ida» y
«vuelta». Los rangos admisibles de estos argumentos serán conocidos y estos objetos por lo general funcionarán
perfectamente bien cuando reciben lo que se espera, en el momento en que se espera. Pero ¿qué pasará cuando
se exceda el tiempo o los argumentos que se pasan estén fuera de los límites?

La solidez de una aplicación se muestra en los casos extremos, gracias a operaciones adaptadas para corregir los
defectos y una correcta protección de los datos. Para conseguir este grado de fiabilidad, antes que cualquier otra
cosa, cada eslabón de la cadena debe permanecer estable, independientemente de sus condiciones de
explotación. Para esto, hay que llevarlos más allá de sus propios límites. El desarrollador deberá imaginar los
peores casos de uso de sus objetos en el momento de su codificación y generar inmediatamente el código más
seguro posible. Para reproducir estos escenarios catastróficos, escribirá una serie de pruebas llamadas pruebas
unitarias, que validarán el correcto comportamiento de cada objeto y abarcarán la mayor parte de código
posible.

Más adelante, los objetos se van a relacionar entre ellos y a intercambiar datos. Las pruebas que validarán estos
intercambios entre objetos se llaman pruebas de integración. En la medida de lo posible, el desarrollador
prepara sus pruebas de integración escribiendo objetos artificiales, con las mismas propiedades que los futuros
componentes reales. Esta etapa permite comprobar que las comunicaciones previstas se han implementado
correctamente, incluso si los objetos artificiales no hacen ninguna otra operación, más allá de generar
alguna traza.

Cuando todos los objetos están validados y se comunican correctamente entre ellos, podemos pasar a las
pruebas de comprobación. Es en este momento cuando se prueba la aplicación completa, haciendo referencia
a las especificaciones funcionales del cliente.

Probar es una tarea ingrata que hay que intentar automatizar lo más posible para que se pueda ejecutar cuando
se desee. Las pruebas se deben escribir con cuidado y deben tener en cuenta todos los casos de uso posibles.

El coste de un bug detectado y arreglado durante la fase de desarrollo es ínfimo respecto al del mismo bug
detectado en producción.

La «testabilidad» debe formar parte de las restricciones durante el análisis orientado a objetos.
Teóricamente, cada módulo se debe poder probar de manera autónoma, lo que implica conseguir un
conjunto de objetos débilmente acoplados entre ellos.

En un mundo ideal, las pruebas unitarias solo deben afectar a los métodos que solo realicen una única
función, sin utilizar de otros métodos.

Estas pruebas, en el pasado poco importantes, se han convertido en primordiales y hay


determinados métodos de desarrollo que empiezan a definirlas antes incluso que a escribir las clases que
se van a probar.

Si está en un proceso de desarrollo o mantenimiento, es muy tranquilizador saber que el conjunto de


pruebas escritas para sus clases se han ejecutado con éxito.
Entorno de ejecución de las pruebas unitarias
Siempre es posible escribir «pequeñas aplicaciones» autónomas que van a permitir verificar los objetos de la
futura «gran aplicación». Por ejemplo, un código que se ha cargado en una consola podrá instanciar la clase que
se tiene que probar y, a continuación, desencadenar una serie de llamadas que muestren mensajes de error o
escriban los resultados en un archivo. Es posible, pero no muy práctico.

IntelliJ IDEA, con el entorno de pruebas Java JUnit 5, simplifica la redacción, ejecución y análisis de las pruebas
unitarias. No hay necesidad de «pequeñas aplicaciones» autónomas; IntelliJ IDEA ofrece directamente la
preparación de un conjunto de pruebas, que el desarrollador podrá reproducir en su totalidad, en grupo (playlist)
o individualmente, gracias al explorador de pruebas. Los resultados de las pruebas se resumen en una vista tipo
«árbol», que utiliza los colores amarillo y verde y permite ir rápidamente a la línea de código que ha fallado en
caso de error. Es posible ejecutar pruebas en modo Debug y, por lo tanto, «trazar» los métodos llamados en los
objetos que se están probando.

Este capítulo solo trata de una pequeña parte de JUnit 5, que es un entorno de prueba muy potente.

A continuación se muestra un ejemplo de resultado de ejecución de dos pruebas.

En la parte superior derecha de la vista, se muestra un trazo rojo ancho, que significa que al menos una prueba
se ha desarrollado de manera incorrecta.

La ejecución de la prueba agregar2 ha fallado. La parte inferior de la pantalla contiene la siguiente información
complementaria:

una explicación de la naturaleza del problema,

un enlace a la línea que presenta el problema en la prueba,

la pila de llamadas (el encadenamiento de métodos que ha provocado el problema).

Por el contrario, la prueba agregar ha tenido éxito. Aparece precedida por una marca verde. El explorador de
pruebas también muestra su tiempo de ejecución de 93 ms.

A continuación se muestra un segundo ejemplo de resultado de ejecución de dos pruebas con éxito.

En la parte superior de la vista, se muestra un trazo verde ancho que significa que todas las pruebas se han
superado con éxito.
El proyecto con pruebas unitarias
Las pruebas son métodos de clases que se añaden, la mayor parte de las veces, al proyecto que contiene las
clases que se deben probar, ayudándose para ello de los asistentes de IntelliJ IDEA.

Hagamos un primer proyecto, siempre de tipo consola, llamado... DemoPruebas, que tenga como nombre
de package demo.prueba.

Añadamos a este proyecto una clase Vector, cuyo contenido sea el siguiente:

package demo.prueba;

public class Vector {

private int x;
private int y;

public int getX() {
return x;
}

public void setX(int x) {
this.x = x;
}

public int getY() {
return y;
}

public void setY(int y) {
this.y = y;
}

public void Agregar(Vector v)
{
x += v.x;
y += v.y;
}
}

La funcionalidad de esta clase es muy sencilla. Vemos dos propiedades x e y con sus descriptores de acceso y un
método que permite agregar los valores de otro objeto Vector. Vamos a construir un método que permita probar
el funcionamiento de este método de adición.
La clase de pruebas
Sitúe su ratón en la clase Vector y llame a la opción Create New Test del menú Navigate.

Confirme la inserción de las pruebas en la misma raíz.

En el asistente de creación de pruebas, seleccione la librería JUnit5 y, a continuación, haga clic en el botón
Fix para arrancar su instalación en el proyecto.

Llame a la clase de pruebas VectorPrueba y guarde el paquete demo.prueba como Destination


package.

Seleccione el método Agregar como método para probar.


Abra la clase VectorPrueba y utilice el asistente «luz roja» para realizar las adiciones necesarias.

El código de partida de nuestra clase de prueba es el siguiente:

package demo.prueba;
import org.junit.jupiter.api.test;

import static org.junit.jupiter.api.Assertions.*;
class VectorPrueba {

@Prueba
void agregar() {
}
}

En él encontramos el paquete que contiene la clase VectorPrueba que, en sí mismo, contiene el método agregar.

Cada método de pruebas se debe preceder por el atributo @Prueba, que lo distinguirá de otros eventuales
métodos de la misma clase.

Puede ser que el asistente haya definido totalmente la ruta del paquete Prueba, escribiendo
@org.junit.jupiter.api.test. Esta escritura se puede reducir como @Prueba delante de cada método de
pruebas y un global import org.junit.jupiter.api.test; como en el código anterior.

A continuación se muestra cómo, una vez que se fija la nomenclatura, se puede crear la prueba en sí misma.
Contenido de un método de prueba
La prueba se va a ejecutar directamente en el entorno de desarrollo y, por lo tanto, no necesita método main de
una clase ni un proyecto particular. El código del método de prueba debe instanciar la clase destino, llamar a uno
de sus métodos y, a continuación, validar el comportamiento esperado. Normalmente, en el marco de pruebas
unitarias, solo debe tener una acción sobre el objeto por cada prueba.

Por ejemplo, para un método Sumar, la prueba comprueba que, si se pasan 2 y 3 como argumentos, entonces el
resultado devuelto será 5.

Las comprobaciones usan la clase Assertions del paquete org.junit.jupiter.api, que ofrece una colección de
métodos que tienen servicios mucho más completos que los asociados a la palabra clave assert, utilizada hasta
entonces en este libro.

Entre este juego de métodos se encuentra el método assertTrue y sus sobrecargas.

(extracto de http://junit.org/junit5/docs/current/api/org/junit/jupiter/api/Assertions.html)

De esta forma, este método permite comprobar que una condición es verdadera y, si no es el caso, devolver un
mensaje en el explorador de pruebas.

Algunos métodos de la clase Assertions ofrecen una versión con mensaje y una versión sin mensaje. Se
aconseja utilizar la versión con mensaje para que el análisis de los problemas sea mucho más rápido.

Ejemplo de utilización de la versión assertTrue con mensaje

@Prueba
void agregar2() {
Vector vector1 = new Vector();
vector1.setX(4);
Vector vector2 = new Vector();
vector2.setX(3);
vector2.setY(3);
vector1.Agregar(vector2);

assertTrue(vector1.getX()==3,
"Error en agregar. Esperado 7 recibido "+vector1.getX());
}

El código del método que hay que probar ha sido modificado para provocar un error en la prueba.

Resultado en el explorador de pruebas:

Ejemplo de utilización de la versión assertTrue sin mensaje

@Prueba
void agregar2() {
Vector vector1 = new Vector();
vector1.setX(4);
Vector vector2 = new Vector();
vector2.setX(3);
vector2.setY(3);
vector1.Agregar(vector2);

assertTrue(vector1.getX()==3);
}

Resultado:
La clase Assertions contiene otros métodos de tipo static, cuya lista no exhaustiva se muestra a continuación.

assertEquals Compara dos objetos. En el caso de objetos que hayan redefinido el


assertNotEquals método equals, entonces este se utilizará automáticamente.

assertSame Compara las referencias de dos objetos.


assertNotSame

assertNull Compara una referencia con NULL.


assertNotNull

assertTrue Prueba una condición.


assertFalse

Una prueba también puede verificar que se produzca una excepción particular, como en el siguiente ejemplo:

@Prueba
void agregarConExcepcion() {

Vector vector1 = new Vector();
vector1.setX(4);
Vector vector2 = null;
try {
vector1.Agregar(vector2);
fail("Se ha debido provocar una excepción.");
}
catch (Exception ex){
assertTrue(ex instanceof NullPointerException,
"Tipo de excepción provocada no esperado." );
}
}

El comando fail permite terminar la prueba con error. En este ejemplo, el flujo de ejecución debería haber
pasado directamente por el catch.

Los extractos de código anteriores forman parte del proyecto DemoPruebas que se puede descargar.
Operaciones de preparación y limpieza
La ejecución de las pruebas se puede preceder por operaciones de inicialización y seguir por operaciones de
«limpieza». Estas operaciones opcionales se escriben en los métodos que utilizan atributos especiales, que
definen en qué momento se ejecutan durante el script.

Atributo del método en


Cuándo se ejecutará el método
JUnit5

@BeforeAll Una vez al inicio de la serie de pruebas de la clase.

@BeforeEach Antes de cada prueba.

@AfterEach Después de cada prueba.

@AfterAll Una vez al final de la ejecución de la serie de pruebas de la


clase.

Extracto de código que muestra la sintaxis de todos los métodos de inicialización

package demo.prueba;

import org.junit.jupiter.api.*;

public class OtrasPruebas {

public OtrasPruebas(){
System.out.println("Constructor de la clase de pruebas");
}

@BeforeAll
public static void Inicialmente(){
System.out.println("Antes de lanzar las pruebas (...)");
}

@AfterAll
public static void Posteriormente(){
System.out.println("Después de todas las pruebas (...)");
}

@BeforeEach
public void antesCada(){
System.out.println("Antes de cada prueba (...)");
}
@AfterEach
public void despuesCada(){
System.out.println("Después de cada prueba (...)");
}

@Prueba
void Prueba1() {
System.out.println("Prueba1 (...)");
}
@Prueba
void Prueba2() {
System.out.println("Prueba2 (...)");
}
@Prueba
void Prueba3() {
System.out.println("Prueba3 (...)");
}

}

A continuación se muestra el resultado de la ejecución del código anterior capturada en la ventana Output:

Los extractos de código anteriores forman parte del proyecto DemoPruebas que se puede descargar.

El método de inicialización de la clase es práctico para preparar los recursos que se utilizan en las pruebas. El
ejemplo clásico es la apertura de una conexión a una base de datos. El método de limpieza de la clase siempre
se llamará, sea cual sea el resultado de las pruebas. Para retomar el caso anterior, contendrá el cierre de la
conexión a la base de datos. Atención, estos dos métodos son de tipo static, lo que hace que solo puedan actuar
sobre los objetos de tipo static.

Los métodos de inicialización y limpieza de las pruebas son de tipo dinámico y se ejecutan antes y después cada
prueba. Encontraremos un código que permite garantizar que todas las pruebas arranquen en las mismas
condiciones.

Como muestra la captura anterior, la clase de pruebas se instancia antes de cada prueba, provocando un
paso obligatorio en los constructores de todos sus datos miembro.
Las pruebas con argumentos externos
Imaginemos que vamos a probar el comportamiento de un método en centenares de casos de uso. Escribir y
mantener la colección de información en el código será difícil y cualquier cambio necesitará la recompilación de
las pruebas: esto no es práctico. Externalizar sus listas en archivos y, a continuación, realizar su carga e
iteraciones en la prueba en sí misma no es factible, pero ¿cuál es la función de la prueba? Basarse en el entorno
de desarrollo para alimentar nuestras pruebas con datos es, de largo, la solución más confortable y esto es lo
que nos ofrece JUnit 5. Se habla por tanto de «data-driven unit tests».

La demostración que sigue utiliza una colección en formato CSV (Comma-Separated Values). Se trata de un
archivo en formato texto muy sencillo, donde cada línea representa una colección de argumentos separados por
comas. Podemos fácilmente obtener un archivo .csv a partir de un archivo Excel, gracias a su función de
exportación.

Implementamos nuestro primer «data-driven unit tests» para comprobar el funcionamiento de un método
que devuelve una cadena de caracteres. Por ejemplo, si pasamos «Hello», debe devolver «olleH».

Añada al proyecto una clase OperacionesCadena que contenga:

package demo.prueba;

public class OperacionesCadena {
public String Retorno(String s){
String r = "";
for(int i=s.length()-1; i>=0; i--){
r += s.charAt(i);
}
return r;
}
}

Cree una clase de prueba como se ha visto anteriormente.

Añada un archivo llamado misArgumentos.csv en la «salida» del proyecto.


En este archivo, necesitamos:

valores de entrada: las cadenas que se van a invertir.

resultados esperados: las cadenas invertidas.

mensajes para visualizar en caso de error.

A continuación se muestra un ejemplo de contenido:

ABCDEF,FEDCBA,Error con ABCDEF


Hello,olleH,Error con Hello
,,Error con null
Cool,looC,Error con Cool

Cada línea representa un caso de prueba. El orden de los campos es importante porque se debe corresponder
con el orden de los argumentos del método de prueba.

void Retorno(String original, String resultadoAComprobar,


String mensajeEnCasoDerror)

A continuación se muestra el código completo de la parte de la prueba:

package demo.prueba;

import org.junit.jupiter.api.test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;

import static org.junit.jupiter.api.Assertions.*;

class OperacionesCadenaPruebas {

@ParameterizedTest
@CsvFileSource(recursos = "misArgumentos.csv")
void Retorno(String original, String resultadoAComprobar,
String mensajeEnCasoDerror) {
OperacionesCadena tct = new OperacionesCadena();

System.out.println("Ejecución del retorno.");
String resultado = "";
try {
resultado = tct.Retorno(original);
assertTrue((original == null) || resultado.equals
(resultadoAComprobar), mensajeEnCasoDerror);
}
catch(Exception ex){
fail("Excepción " + mensajeEnCasoDerror);
}

System.out.println("Resultado del retorno de "+original + ": "+resultado);
}
}

Observe que la «nomenclatura» del método de prueba cambia. Pasa de @Prueba a @ParameterizedTest
completado por un @CsvFileSource(recursos = "misArgumentos.csv").

Puede ser que le falte una librería para que todo esto se compile y funcione.

Para actualizar su proyecto, debe :

Descargar el archivo junit-jupiter-params-5.0.0.jar desde el siguiente enlace:


https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-params/5.0.0
Abrir la estructura del proyecto (menú File - opción Project Structure).

Seleccionar Libraries.

Agregar la librería Java junit-jupiter-params-5.0.0 que acaba de descargar.

Activar los asistentes «luz roja» para realizar las importaciones en el archivo fuente.

Ahora puede lanzar la prueba y observar el siguiente resultado:

Hay una prueba que no ha funcionado. Se trata del número 3, que contiene:

,,Error con null

En efecto, nuestro método de retorno tiene un verdadero defecto cuando la cadena que se le pasa está vacía. Se
trata de un verdadero «bug» porque nunca hay que confiar en los usuarios de nuestros objetos y, por lo tanto,
es preciso comprobar siempre nuestros argumentos antes de la operación.

Vamos a reforzar el código del método como sigue:

package demo.prueba;

public class OperacionesCadena {
public String Retorno(String s){
String r = "";
if( s != null ) {
for (int i = s.length() - 1; i >= 0; i--) {
r += s.charAt(i);
}
}
return r;
}
}
Las suites de pruebas
Ya hemos visto cómo construir clases que contienen métodos de prueba. Estas pruebas se pueden ejecutar
desde IntelliJ IDEA «manualmente». La manipulación puede convertirse rápidamente en algo enrevesado si hay
un número importante de clases de prueba. Además, si decidimos crear una cadena de «build», es decir, un
procedimiento que permita compilar y formatear el producto final, entonces querremos automatizar la ejecución
de las pruebas. Para esto vamos a interesarnos por saber qué es una «suite de pruebas».

Una suite de pruebas es un objeto que va a contener una lista de pruebas para realizar. El objetivo de esta
funcionalidad es automatizar las pruebas para comprobar que las nuevas funcionalidades están activas y que no
hay regresión sobre las antiguas. Podrá construir una «playlist» de pruebas entre todos los métodos que haya
codificado.
Ejercicio

1. Enunciado
Debe escribir:

Una clase ClaseCadena que contenga un método DevuelveIniciales que permita devolver las iniciales
de los nombres y apellidos que se pasan como argumento en forma de cadena, como se indica a
continuación:

String iniciales = ClaseCadena.DevuelveIniciales("Andreas Dulac");


// iniciales debe contener "A.D."

Si el método recibe un argumento incorrecto, debe devolver una cadena vacía.

Una serie de pruebas unitarias permiten comprobar que ninguno de los casos de uso del método
provocan un funcionamiento incorrecto.

2. Corrección
La corrección de este ejercicio está en el proyecto LabPruebas, que se puede descargar.

Los casos de error son los siguientes:

Un argumento de tipo String, por naturaleza, es nullable. El valor null que se pasa como argumento no
debe provocar un funcionamiento incorrecto.

Se debe tener en cuenta el caso de una cadena vacía.

El caso de una cadena que solo contenga una única palabra también se debe probar.

package demo.prueba;

import static org.junit.jupiter.api.Assertions.*;

class ClaseCadenaPrueba {

@org.junit.jupiter.api.test
void PruebaCasoNormal() {
String iniciales = ClaseCadena.DevuelveIniciales
("Andreas Dulac");
// iniciales debe contener "A.D."
assertTrue(iniciales.equals("A.D."), "Esperado: A.D.
Recibido: "+iniciales);
}

@org.junit.jupiter.api.test
void PruebaArgumentoNull()
{
String iniciales = ClaseCadena.DevuelveIniciales(null);
assertTrue(iniciales.equals(""), "PruebaArgumentoNull:
Cadena vacía esperada pero no recibida");
}

@org.junit.jupiter.api.test
void PruebaCadenaVacia()
{
String iniciales = ClaseCadena.DevuelveIniciales("");
assertTrue(iniciales.equals(""), "PruebaCadenaVacia: Cadena vacía esperada pero no recibida ");
}

@org.junit.jupiter.api.test
void PruebaCadenaParcial()
{
String iniciales = ClaseCadena.DevuelveIniciales("Andreas");
assertTrue(iniciales.equals(""), "PruebaCadenaParcial:
Cadena vacía esperada pero no recibida");
}
}

Como desarrollador de código orientado a objetos, probar totalmente su código es capital para su reputación y
la de su empresa. La automatización de las pruebas permite comprobar constantemente que las mejoras y
correcciones no provocan conflictos con la regresión, así que úselas.
10. LA REFLEXIÓN
Introducción
Ahora vamos a interesarnos por otra parte de la POO llamada -con más o menos éxito- reflexión. La «reflexión»
en POO no es el hecho de reflexionar profundamente sobre algo, sino que se trata aquí de una radiografía de
objetos desconocidos y compilados, cuyas propiedades y comportamientos vamos a descubrir sobre la marcha.

Por lo tanto, sería más adecuado asociar a esta «reflexión» el sentido de «reflejo», antes que el de meditación....
Pero ¿para qué hacerlo?
La pregunta es legítima: ¿por qué vamos a necesitar recuperar dinámicamente las características de los objetos?

Dos ejemplos:

El «plug-in»
Ha diseñado una aplicación lúdica para que puedan formarse grandes y pequeños. La aplicación permite
que el jugador se identifique, seleccione un tema para estudiar y divertirse (lo más importante para
aprender). Entonces, la aplicación mide los tiempos de aprendizaje, ejercicios, cuenta las puntuaciones,
las compara y determina incluso recompensas, etc. Un éxito.
Los módulos pedagógicos todavía no están desarrollados porque necesita la participación de especialistas
en geografía, historia, matemáticas, etc. Los jugadores podrán descargar estos nuevos módulos a medida
que estén disponibles. Una vez recuperados, su aplicación los deberá reconocer e integrar, aunque sea
para ignorarlos poco tiempo después. ¿Cómo conseguirlo?
La «reflexión» será una solución elegante porque permitirá a su aplicación recorrer el directorio de
descarga, «radiografiar» dinámicamente los paquetes que contiene para saber si soportan o no la interfaz
Java que ha diseñado para integrarse con su juego (aquí hacemos referencia a las nociones de
polimorfismo e implementación de interfaces que habíamos presentado anteriormente). Si la aplicación
encuentra paquetes compatibles con sus módulos pedagógicos, los carga y los explota para satisfacción
del jugador. Es el principio de los plug-ins. No es necesario que haya archivos de configuración; todo se
hace dinámicamente a través de contratos de interfaces y localización en directorios concretos.

Los «gángsters»
Evidentemente, todo esto es muy bonito, pero la «reflexión» no se detiene en la lista de propiedades y
comportamientos de sus objetos, sino que hay que ser consciente de que el código que ha implementado
y puesto a punto durante meses puede fácilmente hacerse visible para todos y en forma «copiable». Es
suficiente con que su competencia utilice una herramienta de reflexión para que sus trabajos compilados
aparezcan como código fuente en sus pantallas. Incluso aunque «Java» y «código fuente libre»
normalmente están asociados, algunas veces esto puede ser bastante frustrante.
De la misma manera, como es posible recuperar el código fuente de los objetos, también es posible
«romper» los códigos de protección, utilizando código para introducirse en las bases de datos privadas.
Una última decepción: sepa que los atributos de accesibilidad public, protected y private no son más que
un concepto lejano cuando el pirata consigue el control sobre el objeto por reflexión. Si esto le puede
reconfortar, observe que el lenguaje C# presenta los mismos inconvenientes que Java; es el precio que
hay que pagar por la flexibilidad de utilización multiplataforma.
Pero no dramaticemos tan rápido porque existen productos que se pueden aplicar después de la
compilación y que permiten «ofuscar» su código. A esto se le llama «ofuscación». Se trata de disuadir al
pirata. La calidad de la ofuscación depende normalmente del precio del producto de ofuscación. Los
«gratuitos» por lo general tienen sus homólogos en el lado oscuro, que son capaces de anular la
ofuscación.
Otra solución de protección es utilizar componentes llamados «nativos», escritos en lenguaje C, por
ejemplo. El desensamblado también existe, pero la lectura del código generado es mucho más ardua
porque son instrucciones del microprocesador. Por lo tanto, si tiene código que proteger, puede ubicarlo
en DLL nativas escritas en C, que Java llamará sin revelar la lógica interna. La panacea consistiría en
ofuscar también el código Java. El defecto sería que la parte nativa no es portable entre plataformas y
debería proporcionar tantas versiones como sistemas operativos soporte.
Introspección de una clase Java
Gracias a la reflexión, vamos a poder recuperar dinámicamente desde un objeto:

el nombre de la clase original,

si esta clase es de acceso público, privado, etc.,

su clase madre,

qué interfaces soporta esta clase,

su constructor o sus constructores,

sus métodos,

sus propiedades,

información sobre su paquete de pertenencia y sus comentarios,

etc.

Se habla aquí de «metadatos», es decir, de información que describe un tipo de objeto.

Los entornos de desarrollo utilizan la reflexión para ayudarle en la escritura de sus programas.

Es la clase java.Lang.Class la que juega el rol de «recolector de información» entre un tipo de objeto y su
programa. Todos los objetos Java -incluidos los más primitivos- pertenecen a una java.Lang.Class, y enseguida
vamos a radiografiar la de String y pedirle la lista de todos los métodos.

package demo.reflexion;


import java.lang.reflect.Method;

public class Main {

public static void main(String[] args) {

Class stringClass = String.class;

Method[] methods = stringClass.getMethods();

for (Method method: methods) {
System.out.println(method.getName());
}


}
}

Es realmente muy sencillo. Observe que cuando class enumera los métodos de String, devuelve una tabla de
objetos Method -que forman parte de java.lang.reflect- especialistas en la descripción de los métodos.

He aquí una parte de lo que muestra este comando utilizando el bucle de iteración sobre los métodos.

Eche un vistazo a la página https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html para hacerse una


idea de todos los servicios ofrecidos por java.Lang.Class.
Incluso si esto le puede resultar interesante para su aprendizaje de la reflexión, recuperar dinámicamente la lista
de los métodos de la clase String no tiene gran interés porque es la clase Java más conocida y está muy
documentada. Por lo tanto, vamos a ir rápidamente a desarrollar nuestro propio sistema de «plug-in», que va a
permitir:

escanear un directorio buscando archivos Java (archivos .jar),

buscar en los paquetes encontrados las clases que implementan una interfaz dada,

instanciar estas clases,

explotar las instancias de estas clases.

Todo esto en la siguiente sección.


Carga dinámica y utilización de una clase descubierta
La primera etapa de nuestro proyecto consiste en definir el ticket de entrada a nuestro supersistema de plug-in,
es decir, un contrato que todo módulo deberá implementar para poder ser llamado.

Para esto, vamos a crear un paquete que contenga únicamente una interfaz como la siguiente:

public interfaz IPlugin {



String AquiMiNombre();
Boolean JuegaConmigo();
}

Vamos a transformar el archivo .class generado como salida de este proyecto en un archivo .jar, que es el
contenedor de entrega de los archivos Java compilados.

Para esto, vamos a activar el menú File, opción Project Structure, seleccionar Artifacts y después,
pulsando el botón +, agregar un JAR que contenga nuestro módulo y sus dependencias.
La construcción del archivo .jar se realiza desde el menú Build, opción Build Artifacts.

La primera etapa está terminada: tenemos un archivo InterfacePlugin.jar que contiene el contrato. Ahora vamos
a situarnos en el lado del creador de plug-ins y vamos a crear una clase que va a implementar esta interfaz.

En este nuevo proyecto, para poder escribir una clase que implemente una interfaz, hay que agregar su librería.

Para esto, vamos a activar el menú File, opción Project Structure, seleccionar Libraries y después,
pulsando el botón +, agregar la librería Java.
A continuación, podemos escribir el código de nuestro plug-in:

import java.io.IOException;

public class Pluginfo implements IPlugin {


public Pluginfo(){

}

@Override
public String AquiMiNombre() {
return "Cuestionario Informático";
}

@Override
public Boolean JuegaConmigo() {
System.out.println("¿Cuál es su lenguaje favorito?
(1: Java 2: C++ 3: C#)");
try {
int respuesta = System.in.read();
if( respuesta == ’1’ ) {
return true;
}
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
}

Finalmente, transformamos el archivo .class en un archivo plugininfo.jar, siguiendo el procedimiento que se ha


presentado para InterfacePlugin.jar.

Aquí, tenemos un contrato y un plug-in que respeta este contrato. Solo falta codificar la aplicación de juego
capaz de buscarlos en un directorio dado, cargarlos y utilizarlos.

La primera parte de este último trabajo consiste en:

buscar los archivos .jar en un directorio concreto,

extraer los archivos .class de cada archivo .jar leído,

utilizar la reflexión para saber si cada archivo .class contiene una clase que implemente la interfaz
Iplugin.

Para esto, vamos a desarrollar dos bucles anidados: uno que permita recorrer el directorio para buscar archivos
.jar y otro que abra cada archivo .jar encontrado y busque dentro un archivo .class que implemente la interfaz
IPlugin de nuestro juego.

Primer bucle: para buscar los archivos .jar en un directorio dado, vamos a utilizar los servicios de la clase
File, pasando la ruta que contiene nuestros archivos durante su instanciación.

El objeto File instanciado de esta manera nos devuelve la lista de los elementos encontrados en este directorio
gracias a su método listFiles. Durante la iteración en esta lista, vamos a transmitir al segundo bucle los archivos
de extensión .jar e ignorar el resto.

// Definición del directorio donde están los plug-ins


File directorioFuente
= new File("out\\production\\CargadorPlugin\\demo\\reflexion");

// Primer bucle: se recupera el contenido de un directorio
// y se filtran los archivos no deseados
for(File archivoEncontradoEnElDirectorio: directorioFuente.listFiles()) {
// Se filtran los nombres de subdirectorios y los archivos
// que no son .jar
if (archivoEncontradoEnElDirectorio.isDirectory()
|| !archivoEncontradoEnElDirectorio.toString().toLowerCase()
.endsWith(".jar")
continue;

Segundo bucle (anidado en el primero y llamado para cada archivo .jar encontrado):

Vamos a utilizar los servicios de la clase JarFile, que permite recuperar los archivos contenidos en un .jar
como una enumeración de objetos JarEntry. En esta colección, vamos a guardar solo los archivos con
extensión .class.

// Segundo bucle: se recupera el contenido del archivo .jar


JarFile jarFile = new JarFile(archivoEncontradoEnElDirectorio);
Enumeration<JarEntry> archivosEncontradoEnJar = jarFile.entries();

URL[] urls = {new URL("jar:file:" + archivoEncontradoEnElDirectorio + "!/")};
URLClassLoader cl = URLClassLoader.newInstance(urls);

while (archivosEncontradoEnJar.hasMoreElements()) {
JarEntry archivoEncontradoEnJar = archivosEncontradoEnJar.nextElement();
if (archivoEncontradoEnJar.isDirectory()
|| !archivoEncontradoEnJar.getName().toLowerCase().endsWith(".class")) {
continue;
}

Para poder cargar nuestro archivo .class y preguntar si implementa la interfaz IPlugin de nuestro juego,
vamos a utilizar las clases URLClassLoader y Class. Esta última pone a su disposición un método
getInterfaces, que permite devolver una tabla que contiene todas las interfaces de una clase.

// Recuperación del nombre completo de la clase


// (eventualmente precedido por su paquete)
String nombreArchivoEncontradoEnElJar = archivoEncontradoEnJar.getName();
String className
= nombreArchivoEncontradoEnElJar.substring(0,
archivoEncontradoEnJar.getName().length() - ".class".length());
className = className.replace(’/’, ’.’);
// Instanciación de un objeto de tipo Class que describe la clase
//encontrada
Class c = cl.loadClass(className);
// Recuperación de todas las interfaces implementadas por la clase encontrada
Class[] interfaces = c.getInterfaces();
// Busca si una de estas interfaces es IPlugin
for (int i = 0; i < interfaces.length; i++) {
if (interfaces[i].toString().contains("IPlugin")) {
// Este archivo contiene una clase que soporta nuestra interfaz.

Se instancia cada clase que responda al criterio y se invocan sus dos métodos «contractuales» IPlugin. La
instanciación se realiza llamando al constructor recuperado por reflexión.
Observe que sería posible pasar argumentos a este constructor. Observe también que el objeto
instanciado es de tipo Object.

El trabajo de reflexión continúa recuperando e invocando sus dos métodos AquiMiNombre y JuegaConmigo.
Se utilizan los valores de retorno de los dos métodos.

try {
// Recuperación del constructor de la clase que soporta IPlugin
java.lang.reflect.Constructor constructor = c.getConstructor ();
// Instanciación de la clase
Object o = constructor.newInstance (new Object [] {});
// Invocación de su método de identificación
System.out.println(o.getClass().getMethod("AquiMiNombre").invoke(o));
// Eliminación de eventuales caracteres del buffer de teclado
System.in.read(new byte[System.in.available()]);
// Plantea la pregunta y comprueba la respuesta
Boolean PruebaOk = (Boolean)o.getClass().getMethod("JuegaConmigo").invoke(o);;
// Tratamiento de la función de retorno del método invocado
System.out.println(¿PruebaOk? "Bien jugado.":"Es una pena...");
}
catch (NoSuchMethodException e){
}
catch (InstantiationException e){
}
catch (IllegalAccessException e){
}
catch (java.lang.reflect.InvocationTargetException e){
}
catch (IllegalArgumentException e){
}

No nos podemos resistir a ejecutar nuestro programa.


Ejercicio
Cree un nuevo proyecto que contenga una clase que implemente la interfaz IPlugin. Vuelva a copiar su
archivo .jar en el directorio de los plug-ins utilizado con anterioridad y compruebe que la aplicación
CargadorPlugin lo reconoce y explota correctamente.

A continuación se muestra un ejemplo en consola de lo que esto podría dar:


Privado, pero no tanto...
Gracias a los atributos private, hemos visto que haría falta «ocultar» determinados miembros de nuestras clases
para protegerlos de usos malintencionados por parte de otros desarrolladores. Todo esto encaja perfectamente
con la reflexión.

Añada el siguiente método a uno de los plug-ins desarrollados con anterioridad:

private void AquiEsPrivate(){


System.out.println("Este acceso está prohibido ");
}

Añada la siguiente secuencia al administrador de plug-ins:

Method privateMetod = o.getClass().getDeclaredMethod("AquiEsPrivate");


if(privateMetod!= null) {
privateMetod.setAccessible(true);
privateMetod.invoke(o);
}

Vuelva a lanzar la aplicación y compruebe.

Definitivamente, la reflexión es potente y permisiva, pero esto no ha terminado. Además de descubrir los
miembros de las clases, también nos permite leer el código fuente.
Decompilación y ofuscación
Incluso si esto se sale un poco del marco de este libro, esta última sección trata sobre la protección de nuestros
desarrollos contra la copia. Como se ha mencionado anteriormente, los .jar y otras .class se pueden convertir
«fácilmente» en código fuente Java usando herramientas adecuadas. Estas herramientas se llaman
«decompiladores». Se presentan como aplicaciones para instalar en su máquina e incluso como sitios Internet.
Vamos a utilizar uno de ellos: http://www.javadecompilers.com

Recuerde el pequeño ejercicio del capítulo Herencia y polimorfismo, sobre las cuentas bancarias.

En primer lugar, vamos a tomar sus archivos .class -por lo tanto, archivos binarios generados por IntelliJ IDEA-
para pasarlos al descifrador en línea.

Conéctese al sitio web http://www.javadecompilers.com.

En la pantalla de bienvenida, haga clic en el botón Seleccionar archivo o arrastre uno de los archivos
.class a esta zona.

Haga clic en Upload and Decompile (transferir y desensamblar).

Al cabo de algunos instantes, el trabajo ha terminado y puede hacer clic en el botón Save.
El código decompilado ya está en su PC en un archivo .zip. Dentro, el código fuente recuperado es este:

package labcuentabancaria;



public class CuentaBancaria
{
private static Integer numCont = Integer.valueOf(100);
private String Titular;
private Integer Numero;
private double Saldo;

public String getTitular() { return Titular; }

public final void setTitular(String Titular) {
this.Titular = Titular;
}


public Integer getNumero()
{
return Numero;
}

public final void setNumero(Integer Numero) { this.Numero = Numero; }



public double getSaldo()
{
return Saldo;
}

public void setSaldo(double Saldo) { this.Saldo = Saldo; }


public void Ingresar(double credito)
{
Saldo += credito;
}

public void Retirar(double debito)
{
Saldo -= debito;
}



public String toString()
{
return String.format("Cuenta n°%d Titular %s Saldo %.2f euros",
new Object[] { Numero, Titular,

Double.valueOf(Saldo) });
}



public CuentaBancaria(String titular)
{
setTitular(titular);
Integer localInteger1 = numCont;Integer localInteger2 =
CuentaBancaria.numCont = Integer.valueOf(numCont.intValue() +
1);setNumero(localInteger1);
}
}

Compárelo con el original, que es este:

package labcuentabancaria;

public class CuentaBancaria {

// Escribir el tipo estático que guarda
// el contador global del número de cuenta
private static Integer numCont = 100;

// Nombre del titular
private String Titular;
public String getTitular() {
return Titular;
}
public final void setTitular(String Titular) {
this.Titular = Titular;
}

// Número de cuenta de la instancia
private Integer Numero;
public Integer getNumero() {
return Numero;
}
public final void setNumero(Integer Numero) {
this.Numero = Numero;
}

// Saldo de la cuenta
private double Saldo;
public double getSaldo() {
return Saldo;
}
public void setSaldo(double Saldo) {
this.Saldo = Saldo;
}

// Método llamado durante un ingreso en la cuenta
public void Ingresar(double credito) {
Saldo += credito;
}

// Método llamado durante una retirada de la cuenta
public void Retirar(double debito) {
Saldo -= debito;
}

// Devuelve el resumen de la cuenta, retomando
// el método toString de la clase Object
@Override
public String toString(){
return String.format(
"Cuenta n°%d Titular %s Saldo %.2f euros",
Numero, Titular, Saldo);
}

// Constructor de la clase CuentaBancaria
// que guarda el nombre del titular
// y asigna un número de cuenta única
public CuentaBancaria(String titular) {
setTitular(titular);
setNumero(CuentaBancaria.numCont++);
}
}

Aparte de los comentarios, tenemos acceso a todo y esto puede resultar frustrante. Nos podemos reconfortar
pensando que los compañeros que trabajan con C# tienen el mismo problema.

Pero hay herramientas que permiten «borrar las pistas» para que los decompiladores no sean capaces de extraer
el código fuente de manera tan limpia. Sepa sin embargo que está entre la espada y la pared, protegiéndose con
un escudo. Al inicio el escudo resiste, pero se construye otro más poderoso. Este terminará cediendo de nuevo y
se construye uno más grande y fuerte que resiste hasta ... En resumen, descifrar el código solo es una cuestión
de tiempo.

Sin embargo, podemos intentar frenar el asalto pasando nuestros objetos binarios por el tamiz de la ofuscación
digital.

Para esto, vamos a utilizar un producto de ofuscación. Hay muchas herramientas de este tipo y hemos elegido
ProGuard que realiza, entre otras funcionalidades, este tipo de operaciones. ProGuard se puede descargar desde
la siguiente dirección: https://www.guardsquare.com/en/proguard

ProGuard ofrece una interfaz gráfica que se ejecuta desde (...)proguard6.0\bin\proguardgui.bat y se presenta
como sigue:

Los argumentos mínimos para realizar el trabajo de ofuscación son:

un archivo .jar de entrada, que contiene el resultado de la compilación «normal»,

un archivo .jar de salida, que va a contener los archivos ofuscados,

la ubicación de la librería rt.jar.

Hay otros argumentos que permiten configurar la ofuscación, pero vamos a conservar los valores por defecto.
Seleccione la pestaña Input/Output.

Haga clic en Add Input... para definir el archivo .jar de entrada. Si ya ha hecho el ejercicio del capítulo
Herencia y polimorfismo, entonces está en (…)labcuentabancaria\out\artifacts\labcuentabancaria_jar.

Haga clic en Add Ouput... para definir el archivo .jar de salida. Por ejemplo:
(…)labcuentabancaria\out\artifacts\labcuentabancaria_obf.jar.

En el panel inferior, ajuste la ubicación de la librería rt.jar. En caso de una máquina de 64 bits, se
encuentra en C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\jre64\lib.

Seleccione la pestaña Process y haga clic en el botón Process! para lanzar la operación.

Al final, el directorio de trabajo debe contener dos archivos .jar: uno en formato «abierto» y el otro «ofuscado».

En primer lugar, asegúrese de que funcionan los dos en una consola Windows ejecutando:

Para la versión normal:

Java -jar labcuentabancaria.jar

Que muestra:

Cuenta n°100 Titular Víctor Saldo 1000,00 euros


Cuenta n°100 Titular Víctor Saldo 800,00 euros
Cuenta n°101 Titular Andrés Saldo 510,00 euros

Y la versión ofuscada:

Java -jar labcuentabancaria_obf.jar

Que muestra:

Cuenta n°100 Titular Víctor Saldo 1000,00 euros


Cuenta n°100 Titular Víctor Saldo 800,00 euros
Cuenta n°101 Titular Andrés Saldo 510,00 euros

Ahora, usemos los servicios de http://www.javadecompilers.com para recuperar el código decompilado de


la versión ofuscada.

A continuación se muestra el contenido de main.java ofuscado:

package labcuentabancaria;

import java.io.PrintStream;

public class Main
{
public Main() {}

public static void main(String[] paramArrayOfString)
{
(paramArrayOfString = new a("Víctor")).a(1000.0D);
System.out.println(paramArrayOfString.toString());
paramArrayOfString.b(200.0D);
System.out.println(paramArrayOfString.toString());
(paramArrayOfString = new b("Andrés", 2.0D)).a(500.0D);
System.out.println(paramArrayOfString.toString());
}
}

Que se puede comparar con el contenido inicial:

package labcuentabancaria;

public class Main {

public static void main(String[] args) {
// Creación de una cuenta "clásica" para Víctor
CuentaBancaria cb1 = new CuentaBancaria("Víctor");
// Ingreso de 1000€
cb1.Ingresar(1000);
// Impresión del recibo
System.out.println(cb1.toString());

// Retirar 200€
cb1.Retirar(200);
// Impresión del nuevo recibo
System.out.println(cb1.toString());

// Creación de una cuenta "remunerada" al 2% para Andrés
CuentaBancariaRemunerada cb2
= new CuentaBancariaRemunerada("Andrés", 2);
// Ingreso de 500€ inmediatamente aumentado un 2%
cb2.Ingresar(500);
// Impresión del nuevo recibo
System.out.println(cb2.toString());
}
}

Es mucho menos legible.

Como ha podido observar en este capítulo de introducción a la «reflexión», es un medio potente para descubrir y
explotar los objetos dinámicamente. También permite recuperar su código fuente y hay que ser consciente de
ello.

También podría gustarte