INTRODUCCIÓN A LOS OBJETOS
- El progreso de la abstracción
- Todo objeto tiene una interfaz
- Un objeto proporciona servicios
- La implementación oculta
- Reutilización de la implementación
- Herencia
- Relaciones es – un y es – como – un
- Objetos intercambiables con polimorfismo
- La jerarquía de raíz única
- Contenedores
- Tipos parametrizados (genéricos)
- Creación y vida de los objetos
- Tratamiento de excepciones: manejo de errores
- Programación concurrente
- Java e Internet
- ¿Qué es la Web?
- Programación del lado del cliente
- Programación del lado del servidor
- Resumen
Bicicletas para la mente, genial frase de Steve Jobs, eso es lo que son los ordenadores. No son máquinas, son herramientas que permiten ampliar la mente, de este modo las máquinas se van convirtiendo en una forma de expresión como la pintura, la escultura... La programación orientada a objetos (POO) es un medio para utilizar los ordenadores como un medio de expresión.
Este tema es una introducción a la POO.
El progreso de la abstracción
Todos los lenguajes de programación proporcionan abstracciones. El lenguaje ensamblador es una abstracción de la máquina subyacente, muchos de los lenguajes imperativos que le siguieron (Fortran, Basic y C) fueron abstracciones sobre el lenguaje ensamblador. Aunque estos lenguajes supusieron una gran mejora sobre el lenguaje ensamblador, requieren que se piense en términos de la estructura del ordenador en lugar de la estructura del problema que se está intentando resolver. Es decir, hay que establecer una relación entre el espacio de la solución, que es donde se va a implementar un programa (el ordenador), y el espacio del problema, que es el problema que se quiere resolver o programar. Hay que realizar un gran esfuerzo para establecer una correspondencia entre el problema a resolver y la estructura del ordenador.
Surgieron una serie de lenguajes en los que se modelaba el problema a resolver. LISP (todos los problemas se reducen a listas), APL (todos los problemas son algorítmicos), Prolog (todos los problemas se convierten en cadenas de decisión). Estos lenguajes son buenos para resolver los problemas para los que están diseñados, pero tienen dificultades para resolver otra clase de problemas.
La programación orientada a objetos elimina esta dificultad, ya que proporciona herramientas para representar los elementos en el espacio del problema. La POO no está restringida a ningún tipo de problema en particular. Estos elementos son los objetos, palabra mágica. Los elementos que forman parte de un problema se representan mediante objetos. Por tanto, se puede pensar en términos del problema en lugar de en términos del ordenador, lo que facilita enormemente la programación. Cada objeto tiene un estado y dispone de operaciones que pueden ser realizadas por él.
Alan Kay resumió las cinco características básicas de Smalltalk, el primer lenguaje orientado a objetos que tuvo éxito y uno de los lenguajes en los que se basa Java:
Una clase describe un conjunto de objetos con unas características (elementos de datos) y comportamientos idénticos (funcionalidad). Una clase realmente es un tipo de datos. Creamos clases para adaptar un problema en lugar de forzar el uso de un tipo de datos existente. De esta forma ampliamos el lenguaje de programación y se proporciona a las nuevas clases la comprobación de tipos que se proporciona a los tipos predefinidos.
La POO puede reducir una gran cantidad de problemas a una sencilla solución.
Una vez que se crea una clase se pueden crear objetos de esa clase, dichos objetos son los elementos del problema que se está resolviendo, es lo que hacemos con los tipos predefinidos. Uno de los objetivos de la POO es establecer una correspondencia uno a uno de los elementos del problema y los objetos del espacio de la solución.
¿Cómo hacemos que un objeto haga un trabajo útil para el programador? Para ello tenemos que hacer una solicitud al objeto. El objeto sólo puede satisfacer ciertas solicitudes, éstas se definen mediante una interfaz y es el tipo lo que determina la interfaz. Ejemplo:
Tipo: Luz
Interfaz: encender()
apagar()
brillar()
atenuar()
Luz lz=new Luz();
lz.encender();
La interfaz determina qué solicitudes se pueden hacer a un determinado objeto, por tanto debe existir un código en algún lugar que satisfaga estas solicitudes. Ésto, junto con los datos ocultos forman la implementación. Un tipo (clase) tiene un método por cada posible solicitud, cuando hacemos una solicitud a un objeto, se llama al método asociado. El programador envía un mensaje a un objeto (solicitud) y el objeto sabe qué tiene que hacer con el mensaje (ejecución del código).
En el ejemplo Luz es la clase o tipo, lz sería un objeto de esta clase ylas solicitudes son las especificadas en la interfaz: encender, apagar, brillar y atenuar. Se crea un objeto Luz definiendo una “referencia” lz para dicho objeto y se invoca new para realizar solicitudes a este nuevo objeto. Es decir, Luz sería la clase y lz un objeto de esta clase, después de new ya podemos hacer solicitudes al objeto lz. Para enviar un mensaje (solicitud) se utiliza el nombre del objeto seguido de un punto más la solicitud que queremos hacer, en el ejemplo encender.
Un objeto proporciona servicios
Antes de crear una clase, tendremos que tener en cuenta si existe una clase con una funcionalidad parecida, ya que si es así, podemos clonar esta clase y luego añadir o modificar la funcionalidad del clon. Esto se logra con la herencia,pero si la clase original, llamada clase base, superclase o clase padre se modifica, el clon, llamado clase derivada, heredada, subclase o clase hija, también modificamos el clon.
Puede haber más de una clase derivada de la clase base.
Un tipo define las restricciones definidas sobre un conjunto de objetos y tiene una relación con otros tipos. Dos tipos pueden tener características y comportamientos en común, pero uno puede tener más características o manejar más mensajes que el otro o de forma diferente. La herencia expresa esta similitud entre tipos con el concepto de tipo base y derivado. Un tipo base contiene las características y comportamientos que sus tipos derivados comparten. Es recomendable crear un tipo base con un núcleo de características comunes a algunos objetos del sistema. A partir de esta clase base se pueden implementar las diferentes formas este núcleo.
Podemos ver el ejemplo de las formas. Tenemos un tipo base Forma que tiene un tamaño, posición y color, se puede dibujar, borrar, colorear, etc. A partir del tipo base Forma se pueden crear formas específicas cada una con sus características propias, el triángulo, el cuadrado, el círculo, etc, por ejemplo el área se calcula de forma diferente para cada forma. La jerarquía de tipos engloba tanto las similitudes como las diferencias.
Cuando tenemos un problema y podemos representar la solución en términos del problema, no necesitamos muchos modelos intermedios para pasar de la descripción del problema a la descripción de la solución. Con objetos, la jerarquía de tipos es el modelo principal, ya que podemos pasar directamente de la descripción del problema a la descripción de la solución. Con la POO es demasiado sencillo ir del principio al final.
Cuando heredamos de una clase base se crea una nueva clase con todos los miembros de la clase base (excepto privados) y además, algo que es muy importante, se duplica la interfaz de la clase base, es decir, todos los mensajes que se pueden enviar a la clase base se pueden enviar a la clase derivada, las clases derivadas son del mismo tipo que la clase base. Un círculo es una forma. Esta equivalencia de tipos a través de la herencia es un camino fundamental para comprender el significado de la POO. Hay dos formas de diferenciar la clase base de la derivada:
Los lenguajes POO utilizan acoplamiento tardío. Java emplea un bit de código especial en lugar de una llamada absoluta. Este código utiliza la información almacenada en el objeto para obtener la dirección del método. Así cada objeto se comporta de acuerdo al contenido del bit de código especial. Cuando se envía un mensaje a un objeto, éste sabe lo que tiene que hacer con dicho mensaje.
Para ver cómo funciona el polimorfismo tenemos el siguiente fragmento de código:
El progreso de la abstracción
Todos los lenguajes de programación proporcionan abstracciones. El lenguaje ensamblador es una abstracción de la máquina subyacente, muchos de los lenguajes imperativos que le siguieron (Fortran, Basic y C) fueron abstracciones sobre el lenguaje ensamblador. Aunque estos lenguajes supusieron una gran mejora sobre el lenguaje ensamblador, requieren que se piense en términos de la estructura del ordenador en lugar de la estructura del problema que se está intentando resolver. Es decir, hay que establecer una relación entre el espacio de la solución, que es donde se va a implementar un programa (el ordenador), y el espacio del problema, que es el problema que se quiere resolver o programar. Hay que realizar un gran esfuerzo para establecer una correspondencia entre el problema a resolver y la estructura del ordenador.
Surgieron una serie de lenguajes en los que se modelaba el problema a resolver. LISP (todos los problemas se reducen a listas), APL (todos los problemas son algorítmicos), Prolog (todos los problemas se convierten en cadenas de decisión). Estos lenguajes son buenos para resolver los problemas para los que están diseñados, pero tienen dificultades para resolver otra clase de problemas.
La programación orientada a objetos elimina esta dificultad, ya que proporciona herramientas para representar los elementos en el espacio del problema. La POO no está restringida a ningún tipo de problema en particular. Estos elementos son los objetos, palabra mágica. Los elementos que forman parte de un problema se representan mediante objetos. Por tanto, se puede pensar en términos del problema en lugar de en términos del ordenador, lo que facilita enormemente la programación. Cada objeto tiene un estado y dispone de operaciones que pueden ser realizadas por él.
Alan Kay resumió las cinco características básicas de Smalltalk, el primer lenguaje orientado a objetos que tuvo éxito y uno de los lenguajes en los que se basa Java:
- Todo es un objeto.
- Un programa es un montón de objetos que se dicen entre sí lo que tienen que hacer enviándose mensajes.
- Cada objeto tiene su propia memoria formada por otros objetos.
- Todo objeto tiene un tipo asociado.
- Todos los objetos de un tipo particular pueden recibir los mismos mensajes.
- Un objeto tiene estado comportamiento e identidad.
Una clase describe un conjunto de objetos con unas características (elementos de datos) y comportamientos idénticos (funcionalidad). Una clase realmente es un tipo de datos. Creamos clases para adaptar un problema en lugar de forzar el uso de un tipo de datos existente. De esta forma ampliamos el lenguaje de programación y se proporciona a las nuevas clases la comprobación de tipos que se proporciona a los tipos predefinidos.
La POO puede reducir una gran cantidad de problemas a una sencilla solución.
Una vez que se crea una clase se pueden crear objetos de esa clase, dichos objetos son los elementos del problema que se está resolviendo, es lo que hacemos con los tipos predefinidos. Uno de los objetivos de la POO es establecer una correspondencia uno a uno de los elementos del problema y los objetos del espacio de la solución.
¿Cómo hacemos que un objeto haga un trabajo útil para el programador? Para ello tenemos que hacer una solicitud al objeto. El objeto sólo puede satisfacer ciertas solicitudes, éstas se definen mediante una interfaz y es el tipo lo que determina la interfaz. Ejemplo:
Tipo: Luz
Interfaz: encender()
apagar()
brillar()
atenuar()
Luz lz=new Luz();
lz.encender();
La interfaz determina qué solicitudes se pueden hacer a un determinado objeto, por tanto debe existir un código en algún lugar que satisfaga estas solicitudes. Ésto, junto con los datos ocultos forman la implementación. Un tipo (clase) tiene un método por cada posible solicitud, cuando hacemos una solicitud a un objeto, se llama al método asociado. El programador envía un mensaje a un objeto (solicitud) y el objeto sabe qué tiene que hacer con el mensaje (ejecución del código).
En el ejemplo Luz es la clase o tipo, lz sería un objeto de esta clase ylas solicitudes son las especificadas en la interfaz: encender, apagar, brillar y atenuar. Se crea un objeto Luz definiendo una “referencia” lz para dicho objeto y se invoca new para realizar solicitudes a este nuevo objeto. Es decir, Luz sería la clase y lz un objeto de esta clase, después de new ya podemos hacer solicitudes al objeto lz. Para enviar un mensaje (solicitud) se utiliza el nombre del objeto seguido de un punto más la solicitud que queremos hacer, en el ejemplo encender.
Un objeto proporciona servicios
Una buena forma de pensar en los objetos es como proveedores de servicios. Un programa proporciona servicios al usuario y esto es posible gracias a los servicios de otros objetos. El objetivo es obtener un conjunto de objetos que faciliten los servicios para resolver el problema.
A la hora de resolver un problema nos preguntamos, ¿qué objetos necesito para resolver mi problema de la forma más simple? Debemos pensar en el conjunto de objetos necesario, algunos de estos objetos ya existirán y los que no existen, ¿cómo deben ser? ¿Qué servicios deben proporcionar y qué objetos necesitarán para cumplir con sus obligaciones? Con este planteamiento llegaremos a un punto en el que podremos decir: “Este objeto es lo suficientemente sencillo para escribirlo yo” o “Estoy seguro de que este objeto existe”.
Si pensamos en un objeto como un proveedor de servicios tenemos una ventaja adicional: ayuda a mejorar la cohesión del objeto. Una alta cohesión es una propiedad muy importante en el diseño de software y significa que los diferentes aspectos de un componente software (objeto, método o biblioteca de objetos) deben ajustar bien entre sí. Un problema que nos encontramos a la hora de diseñar un objeto es que le podemos asignar demasiada funcionalidad. Cada objeto tiene un conjunto cohesivo de servicios que ofrecer. En un buen diseño orientado a objetos, cada objeto hace una cosa bien sin intentar hacer demasiadas cosas.
Tratar los objetos como proveedores de servicios es útil durante el proceso de diseño y también para entender el propio código o para reutilizar un objeto. Si se es capaz de ver el valor de un objeto según el servicio que proporciona, será mucho más fácil adaptarlo al diseño.
La implementación oculta
Vamos a distinguir entre creadores de clases (aquellos que crean nuevos tipos de datos) y programadores de clientes (aquellos que utilizan las clases que crean los creadores de clases). El objetivo del programador de clases es recopilar las herramientas necesarias para el desarrollo de aplicaciones. El objetivo del creador de clases es construir clases que aporten al programador cliente lo necesario para programar y oculte todo lo demás, lo innecesario. El programador cliente no puede acceder a lo oculto y así el creador de clases puede cambiar esta parte cuando sea necesario sin preocuparse del impacto que la modificación pueda implicar. La parte oculta representa las vulnerabilidades internas de un objeto y un cliente poco cuidadoso o con poca formación podría corromper fácilmente.
En toda relación deben establecerse unos límites. Al crear una biblioteca se establece una relación con el programador de clientes, que es un programador que utiliza la biblioteca para crear sus aplicaciones. Si todos los miembros de una clase estuvieran disponibles para cualquiera, el programador de clientes podría modificar dicha clase y así no habría forma de imponer unas reglas, por tanto, hay que imponer un control de acceso.
La primera razón para el control de acceso es mantener las manos de los programadores de clientes apartadas de las partes que son necesarias para la operación interna de los tipos de datos, pero no de la interfaz necesaria para resolver problemas concretos. Los programadores de clientes ven lo realmente importante para ellos y no demás se ignora.
La segunda razón es permitir al diseñador de clases cambiar el funcionamiento interno de la clases sin preocuparse de cómo afectará al programador de clientes.
Java emplea tres palabras clave para definir los límites en una clase: public, private y protected. Estos modificadores de acceso determinan quien puede usar las definiciones:
- public. El elemento está disponible para todo el mundo.
- private. Nadie excepto el creador del tipo puede acceder a él. Si se intenta acceder a un elemento private se obtendrá un error de compilación.
- protected. Actúa como private excepto que una clase heredada tiene acceso a los miembros protected pero no a los private.
Cuando no se emplea ninguno de estos modificadores Java utiliza el acceso de paquete, en el que las clases pueden acceder a los miembros de otras clases que pertenecen al mismo paquete y fuera del paquete estos miembros aparecen como private.
Reutilización de la implementación
Una vez que hemos creado una clase debería poder reutilizarse en diferentes programas. Sin embargo, esto no es tan fácil, es necesario experiencia y perspicacia para obtener objetos reutilizables. Una de las ventajas de la POO es la reutilización de código.
La forma más sencilla de reutilizar una clase consiste en emplear directamente un objeto de dicha clase, pero hay otras formas de reutilizar clases creadas por nosotros, la composición, la agregación y la herencia.
- Composición. Consiste en crear una clase a partir de objetos de otras clases (objetos miembro), así obtendremos una nueva clase con la funcionalidad deseada. Los objetos que conforman la clase son necesarios para su existencia y esta clase maneja los ciclos de vida de los objetos componentes. La composición es una relación "tiene un" como en "un coche tiene un motor". Los objetos miembro de la nueva clase normalmente son privados e inaccesibles para los programadores de clientes.
- Agregación. Si la composición se realiza de forma dinámica se denomina Agregación, es decir, la clase puede tomar objetos como parámetro en el constructor o en algún método, de esta forma se puede cambiar dinámicamente el comportamiento del programa. La agregación es una relación "un coche tiene un maletero con un nº de bultos variable", el número de bultos puede ir como parámetro en el constructor de la clase Coche o como parámetro de un método.
- Herencia. Consiste en crear una clase a partir de una clase existente. Así aprovechamos la funcionalidad de una clase existente, no tenemos que crear una nueva clase a partir de cero y además le podemos añadir más funcionalidad. La herencia se utiliza cuando es necesario, no hay que emplearla siempre, ya que esto puede llevar a diseños complejos y complicados. Antes de crear una nueva clase es recomendable utilizar la composición si es posible, antes que la herencia.
Antes de crear una clase, tendremos que tener en cuenta si existe una clase con una funcionalidad parecida, ya que si es así, podemos clonar esta clase y luego añadir o modificar la funcionalidad del clon. Esto se logra con la herencia,pero si la clase original, llamada clase base, superclase o clase padre se modifica, el clon, llamado clase derivada, heredada, subclase o clase hija, también modificamos el clon.
Puede haber más de una clase derivada de la clase base.
Un tipo define las restricciones definidas sobre un conjunto de objetos y tiene una relación con otros tipos. Dos tipos pueden tener características y comportamientos en común, pero uno puede tener más características o manejar más mensajes que el otro o de forma diferente. La herencia expresa esta similitud entre tipos con el concepto de tipo base y derivado. Un tipo base contiene las características y comportamientos que sus tipos derivados comparten. Es recomendable crear un tipo base con un núcleo de características comunes a algunos objetos del sistema. A partir de esta clase base se pueden implementar las diferentes formas este núcleo.
Podemos ver el ejemplo de las formas. Tenemos un tipo base Forma que tiene un tamaño, posición y color, se puede dibujar, borrar, colorear, etc. A partir del tipo base Forma se pueden crear formas específicas cada una con sus características propias, el triángulo, el cuadrado, el círculo, etc, por ejemplo el área se calcula de forma diferente para cada forma. La jerarquía de tipos engloba tanto las similitudes como las diferencias.
Cuando tenemos un problema y podemos representar la solución en términos del problema, no necesitamos muchos modelos intermedios para pasar de la descripción del problema a la descripción de la solución. Con objetos, la jerarquía de tipos es el modelo principal, ya que podemos pasar directamente de la descripción del problema a la descripción de la solución. Con la POO es demasiado sencillo ir del principio al final.
Cuando heredamos de una clase base se crea una nueva clase con todos los miembros de la clase base (excepto privados) y además, algo que es muy importante, se duplica la interfaz de la clase base, es decir, todos los mensajes que se pueden enviar a la clase base se pueden enviar a la clase derivada, las clases derivadas son del mismo tipo que la clase base. Un círculo es una forma. Esta equivalencia de tipos a través de la herencia es un camino fundamental para comprender el significado de la POO. Hay dos formas de diferenciar la clase base de la derivada:
- Se añaden nuevos métodos a la clase derivada. La clase base no hacía todo necesario y por eso se añaden estos nuevos métodos. Este uso de la herencia puede ser la solución perfecta para solucionar el problema que se tiene entre manos. Pero debemos considerar la posibilidad de que la clase base necesite utilizar los métodos de la clase derivada.
- Se modifican métodos existentes en la clase base. Para cambiar el comportamiento de un método (sustitución del método) simplemente se crea una definición para el mismo en la clase derivada.
Con las relaciones de herencia nos podemos encontrar dos casos:
- Relaciones es – un. En este caso la clase derivada sólo sustituye a los métodos de la clase base y no añade métodos nuevos. De esta forma la clase derivada es exactamente del mismo tipo que la clase base, es posible sustituir un objeto de la clase derivada por uno de la clase base. Se trata de una relación es – un, ya que podemos decir, por ejemplo, que un triángulo es una forma, un círculo es una forma.
- Relaciones es – como – un. Hay veces que es necesario que la clase derivada añada nuevos métodos diferentes de los de la clase base. El tipo derivado podría ser sustituido por su clase base pero la sustitución no es perfecta ya que los objetos de la clase base no pueden acceder a los nuevos métodos de la clase derivada. Esta relación es del tipo es – como – un, es decir, la nueva clase derivada contiene los métodos de la clase base pero añade otros nuevos, por tanto, no son clases iguales.
La experiencia nos ayudará a saber qué tipo de relación debemos usar en cada caso.
Objetos intercambiables con polimorfismo
Cuando trabajamos con jerarquía de objetos, a veces nos interesa tratar a los objetos como su tipo base y no como el tipo específico que es. Esto permite escribir código que no dependa de tipos específicos. En el ejemplo de las formas podemos dibujar, borrar, mover un círculo, triángulo, cuadrado, sin preocuparnos de qué forma se trata ya que los métodos envían un mensaje al objeto forma.
La adición de tipos nuevos no afecta al código, es más, esta es la forma común de ampliar un programa orientado a objetos para manejar situaciones nuevas. Por ejemplo, podemos tener un tipo nuevo Pentágono que derive de Forma sin modificar los métodos asociados sólo a las formas genéricas. Esto mejora los diseños y reduce el costo del mantenimiento del software.
Sin embargo, tenemos un problema cuando un objeto derivado se trata como un objeto de la clase base, un círculo como una forma, el compilador no puede saber en tiempo de compilación qué parte del código tiene que ejecutar.
En un lenguaje no - POO la llamada a una función hace un acoplamiento temprano, el compilador genera una llamada a una función específica y el sistema de tiempo de ejecución resuelve esta llamada a la dirección absoluta del código que se va a ejecutar. En POO no se puede saber la dirección del código hasta la ejecución, es necesario otro esquema.Los lenguajes POO utilizan acoplamiento tardío. Java emplea un bit de código especial en lugar de una llamada absoluta. Este código utiliza la información almacenada en el objeto para obtener la dirección del método. Así cada objeto se comporta de acuerdo al contenido del bit de código especial. Cuando se envía un mensaje a un objeto, éste sabe lo que tiene que hacer con dicho mensaje.
Para ver cómo funciona el polimorfismo tenemos el siguiente fragmento de código:
Este método tiene un argumento del tipo base Forma que sirve para las clases derivadas Circulo, Cuadrado, Triangulo y si se añade un tipo nuevo como Hexagono a través de herencia, el código funcionará para el tipo nuevo y los existentes. El programa es ampliable.
Podemos, por tanto, escribir lo siguiente:
Y funcionará correctamente. Al método hacerAlgo le estamos pasando un círculo, un cuadrado o un triangulo y estos objetos son Formas. Tratar un tipo derivado como su clase base se denomina generalización (upcasting), proyección hacia arriba, y tiene en cuenta la forma en que se dibujan los diagramas de herencia. Upcasting es por tanto efectuar una proyección sobre un tipo base ascendiendo por el diagrama de herencia.
Un programa orientado a objetos siempre tiene una generalización, ya que de esta forma no tenemos que conocer el tipo exacto de la clase con la que se trabaja.
El código de hacerAlgo nos dice: "Eres una forma y te puedo borrar y dibujar teniendo en cuenta correctamente los detalles".
Cuando se llama al método hacerAlgo independientemente del tipo de forma que introduzcamos como argumento, hace lo correcto. Esto ocurre en tiempo de ejecución ya que en tiempo de compilación el método hacerAlgo no sabe con qué tipos está tratando. Todo esto ocurre gracias al polimorfismo.
La jerarquía de raíz única
En Java todas las clases heredan de una única clase raíz, la clase Object. La jerarquía de raíz única tiene muchas ventajas.
Todos los objetos de una jerarquía de raíz única tienen una interfaz en común y por tanto, al final son del mismo tipo fundamental.
Todos los objetos de una jerarquía de raíz única tienen una determinada funcionalidad. Se pueden hacer determinadas operaciones sobre todos los objetos del sistema. También facilita la implementación de un depurador de memoria. Todos los objetos tienen un tipo determinado.
formas=new ArrayList();
Podemos, por tanto, escribir lo siguiente:
Y funcionará correctamente. Al método hacerAlgo le estamos pasando un círculo, un cuadrado o un triangulo y estos objetos son Formas. Tratar un tipo derivado como su clase base se denomina generalización (upcasting), proyección hacia arriba, y tiene en cuenta la forma en que se dibujan los diagramas de herencia. Upcasting es por tanto efectuar una proyección sobre un tipo base ascendiendo por el diagrama de herencia.
Un programa orientado a objetos siempre tiene una generalización, ya que de esta forma no tenemos que conocer el tipo exacto de la clase con la que se trabaja.
El código de hacerAlgo nos dice: "Eres una forma y te puedo borrar y dibujar teniendo en cuenta correctamente los detalles".
Cuando se llama al método hacerAlgo independientemente del tipo de forma que introduzcamos como argumento, hace lo correcto. Esto ocurre en tiempo de ejecución ya que en tiempo de compilación el método hacerAlgo no sabe con qué tipos está tratando. Todo esto ocurre gracias al polimorfismo.
La jerarquía de raíz única
En Java todas las clases heredan de una única clase raíz, la clase Object. La jerarquía de raíz única tiene muchas ventajas.
Todos los objetos de una jerarquía de raíz única tienen una interfaz en común y por tanto, al final son del mismo tipo fundamental.
Todos los objetos de una jerarquía de raíz única tienen una determinada funcionalidad. Se pueden hacer determinadas operaciones sobre todos los objetos del sistema. También facilita la implementación de un depurador de memoria. Todos los objetos tienen un tipo determinado.
Contenedores
Generalmente no sabemos los objetos que vamos a necesitar para resolver un problema, ni el tiempo que va a llevar. Tampoco cómo se van a almacenar dichos objetos ya que dicha información sólo se conoce en tiempo de ejecución.
La solución en POO es crear un tipo de objeto denominado contenedor, este objeto almacena referencias a otros objetos. Una matriz también permite almacenar referencias a otros objetos, pero tiene un tamaño especificado en tiempo de compilación. En un contenedor, el tamaño se amplía cuando es necesario, no hay que declarar el tamaño en el programa, sino que sólo se declara el contenedor y él se ocupa de todo.
En Java nos encontramos varios tipos de contenedores, veamos algunos ejemplos: List almacena secuencias, Maps asocia objetos con otros objetos, Sets almacena un objeto de cada tipo y otros componentes como colas, árboles, pilas, etc…
Normalmente querremos tener un contenedor que nos resuelva nuestro problema. Sin embargo, hay dos razones por las que nos interesará disponer de varios contenedores:
- Cada contenedor tiene su propia interfaz y comportamiento. Una pila es diferente de una cola, etc. Es posible que uno de estos contenedores nos sea más útil que los restantes.
- Diferentes contenedores tienen una eficiencia distinta a la hora de realizar determinadas operaciones. Por ejemplo, en el tipo List tenemos los contenedores ArrayList (lista matricial) y LinkedList (lista enlazada). Para acceder aleatoriamente a un elemento utilizaremos ArrayList, ya que es una operación de tiempo constante y para LinkedList deberíamos desplazarnos a lo largo de la lista. Sin embargo, para insertar un elemento en medio de la lista es más barato hacerlo en LinkedList que ArrayList.
Debido a la abstracción de la interfaz List, podremos cambiar de un contenedor a otro con un impacto mínimo en el código.
Tipos parametrizados (genéricos)
Antes de Java SE5 los contenedores albergaban objetos del tipo universal Object. Así, un contenedor puede almacenar cualquier tipo de objeto.
En estos contenedores se añaden referencias a objetos que luego se extraen. Como los contenedores almacenan objetos de tipo Object, al añadir una referencia a un objeto se convierte en una referencia a Object perdiendo así su carácter. Al extraer la referencia, se obtiene una referencia a Object y no al tipo almacenado. ¿Cómo haremos para transformar esa referencia al tipo específico almacenado en el contenedor?
Lo que se hace es efectuar una especialización, descendiendo por la jerarquía hasta alcanzar un tipo más específico, esto se denomina especialización (downcasting). Una especialización es más complicada que una generalización (upcasting) ya que no todo objeto de tipo Object es necesariamente de tipo Círculo o Forma.
Esta conversión no es peligrosa ya que si hacemos una conversión de tipos incorrecta, obtendremos un error en tiempo de ejecución llamado excepción. Cuando extraemos un objeto de un contenedor tendremos que recordar de qué tipo es exactamente el objeto para realizar la conversión de forma correcta.
Tanto la especialización como las comprobaciones en tiempo de ejecución requieren tiempo adicional para la ejecución del programa y un mayor esfuerzo del programador. Lo lógico sería tener un contenedor que supiese el tipo de elementos que maneja. La solución a esto es el mecanismo de tipos parametrizados. Un tipo parametrizado es una clase que el compilador personaliza para que funcione con cada tipo concreto. Así se puede crear un contenedor personalizado para objetos Forma.
En Java SE5 tenemos tipos parametrizados, que se llaman genéricos en Java. La especificación de tipos se realiza de la siguiente manera, para crear un contenedor de tipo ArrayList que almacene objetos de tipo Forma:
ArrayList
Creación y vida de los objetos
Un objeto necesita una serie de recursos, generalmente memoria, para existir. Una vez que el objeto deja de ser necesario hay que liberar estos recursos.
¿Dónde se almacenan los datos de un objeto y cómo se controla su tiempo de vida? En C++ la eficiencia es lo más importante y todo queda en manos del programador. Para conseguir la máxima velocidad en la ejecución, el almacenamiento y el tiempo de vida del objeto se determinan mientras se escribe el programa colocando los objetos en la pila o en el área de almacenamiento estático. De esta forma la velocidad de asignación y liberación del almacenamiento pasa a ser prioritario y este control puede resultar muy útil en muchas situaciones. El problema es que perdemos flexibilidad, ya que es preciso conocer cuando escribimos el programa la cantidad, tiempo de vida y tipo de los objetos. En muchas ocasiones esta solución es muy restrictiva.
Otra posibilidad es crear los objetos dinámicamente en un área de memoria denominada cúmulo. Así no sabemos hasta la ejecución del programa cuántos objetos van a ser necesarios, su tiempo de vida y su tipo. Cuando necesitamos un nuevo objeto se crea en el cúmulo. Como el almacenamiento se gestiona dinámicamente en tiempo de ejecución, el tiempo necesario para asignar memoria es bastante mayor en el cúmulo que en la pila.
Java utiliza un mecanismo dinámico de asignación de memoria. Cuando queremos crear un objeto utilizamos new para construir una instancia dinámica de un objeto.
Ahora tenemos otro problema. En aquellos lenguajes en los que se crean objetos en la pila, el compilador determina cuando se deben destruir estos objetos, pero cuando se crea el objeto en el cúmulo, el compilador no sabe cuál es su tiempo de vida. Para ello Java utiliza el depurador de memoria, que descubre cuando un objeto ya no está en uso y lo destruye. Con el depurador el programador no debe controlar tantos problemas y se reduce el código a escribir. Además, proporciona una protección mucho mayor contra las pérdidas de memoria muy comunes en lenguajes como C++, en los que si no se destruye correctamente algún objeto, puede provocar pérdidas de memoria.
En Java el depurador de memoria sabe cuándo un objeto ya no es necesario y libera la memoria de ese objeto. Esta herramienta junto con la de que todos los objetos heredan de una raíz única, Object, y junto a la característica de que todos los objetos se crean en el cúmulo de memoria, hacen de Java un lenguaje más sencillo, con menos decisiones que tomar y menos problemas por tanto.
Tratamiento de excepciones: manejo de errores
Desde la aparición de los lenguajes de programación, el tratamiento de errores ha sido un problema difícil de tratar. Es complicado diseñar un buen sistema de tratamiento de errores.
Los mecanismos de tratamiento de excepciones integran en el lenguaje de programación o incluso en los sistemas operativos la gestión de los errores. Una excepción es un objeto generado donde se produce el error, y puede ser capturado por una rutina de tratamiento de excepciones diseñada para tratar ese tipo particular de error. Es como una ruta de ejecución diferente que se toma cuando se produce la excepción, y esta ruta diferente no interfiere con el código que se ejecuta normalmente.
Las excepciones no pueden ignorarse, como puede ocurrir con los típicos valores de error devueltos por los métodos o por los indicadores activados por los métodos para avisar de un error, esto significa que van a ser tratadas. Además proporcionan un mecanismo para recuperarse de cualquier situación errónea. En lugar de salir del programa, a menudo podemos restaurar el programa y continuar con su ejecución. Así tenemos programas más robustos.
En Java el tratamiento de excepciones estaba previsto desde el principio y estamos obligados a utilizarlo. Es el único mecanismo que nos informa de la existencia de errores. Si no escribimos el código de manera que trate las excepciones, obtendremos un error en tiempo de compilación.
El tratamiento de excepciones ya existía antes de la aparición de los lenguajes orientados a objetos.
Programación concurrente
Un concepto fundamental en la programación es poder gestionar más de una tarea al mismo tiempo. Muchos problemas de programación requieren que se pare la tarea que se está llevando a cabo, se resuelva el problema y luego se vuelva de nuevo a la tarea.
A veces las interrupciones son necesarias para atender tareas con requisitos críticos de tiempo, sin embargo, lo habitual es que nos interese dividir el problema en una serie de fragmentos (tareas) que se ejecuten por separado, así el programa tiene un mejor tiempo de respuesta. Estos fragmentos son las hebras y el conjunto general se llama concurrencia. Un ejemplo de concurrencia es la interfaz de usuario, mientras se ejecuta una tarea podemos pulsar a un botón para realizar otra sin tener que esperar a que finalice la tarea que estaba realizando.
Si tenemos un sólo procesador, éste va repartiendo el tiempo disponible entre las distintas tareas. Si tenemos más de un procesador, se puede asignar una tarea a cada procesador, así las tareas se ejecutan en paralelo. Una de las ventajas de introducir la concurrencia en un lenguaje de programación es que el programador se despreocupa de si tenemos uno o varios procesadores. El problema se divide en una serie de tareas, si el ordenador dispone de más de un procesador el programa se ejecutará más rápido, sin necesidad de realizar ningún ajuste.
Con la concurrencia tenemos un problema, los recursos compartidos. Varias tareas pueden intentar acceder al mismo tiempo a un mismo recurso. Para resolver este problema, hay que bloquear el recurso cuando está siendo utilizado por una de las tareas. En el momento en el que la tarea acaba de utilizar el recurso, éste se desbloquea para que otra tarea pueda utilizarlo.
En Java la concurrencia está integrada dentro del lenguaje.
Java e Internet
¿Por qué Java no es un lenguaje de programación más y resulta una verdadera revolución? La respuesta es que, aunque permite resolver problemas de programación en entornos autónomos, su importancia se debe a que resuelve los problemas de programación que surgen en la World Wide Web.
¿Qué es la web?
Vamos a ver algunos conceptos para entender lo que es la Web.
Informática cliente/servidor
La idea principal de los sistemas cliente/servidor es que si disponemos de un repositorio centralizado de información lo podemos distribuir bajo demanda a una serie de personas o computadoras. Este repositorio de información está centralizado, por lo que si necesitamos modificarlo, estas modificaciones no afectarán a los consumidores de la información. El repositorio de información, el software que distribuye la información y las máquinas donde se almacenan tanto la información como el software se denomina servidor. El software de las máquinas consumidoras, que se comunica con el servidor, extrae la información, la procesa y la muestra en la propia máquina se denomina cliente.
El problema surge cuando tenemos un servidor al que intentan acceder múltiples clientes al mismo tiempo. Normalmente se utiliza un sistema gestor de base de datos que distribuye los datos entre diferentes tablas, así se optimiza el uso de los datos. Estos sistemas a menudo permiten introducir nueva información en el servidor. Hay que garantizar que estos nuevos datos no sobrescriban los nuevos datos de otros clientes y que no se pierdan datos al añadirlos a la base de datos. A medida que va cambiando el software del servidor es necesario diseñar, depurar e instalar el software en las máquinas clientes, lo cual es caro y complicado. Tenemos en los clientes diferentes tipos de computadores y de sistemas operativos. Hay que tener en cuenta también el rendimiento: podemos tener cientos de clientes enviando solicitudes al servidor, cualquier retraso puede ser crítico. Para minimizar la latencia, los programadores hacen un gran esfuerzo para tratar de descargar las tareas de procesamiento, bien en las máquinas cliente o bien en otras máquinas situadas junto al servidor que utilizan un tipo especial de software llamado middleware.
La distribución de la información tiene tantos niveles de complejidad que parece un problema insoluble. Sin embargo, la informática cliente/servidor representa la mitad de las actividades de programación hoy en día. Esta arquitectura es la responsable de muchas tareas, como por ejemplo, la realización de pedidos, transacciones con tarjetas de crédito, y la distribución de datos como información bursátil, datos científicos, datos de organismos gubernamentales, etc. En el pasado se han ido desarrollando soluciones individuales a los problemas según iban surgiendo. El usuario se veía obligado a aprender nuevas interfaces en cada caso. Por tanto se llegó a un punto en el que había que resolver el problema de la informática cliente/servidor de una vez.
La Web como un gigantesco servidor
La Web es un sistema gigantesco cliente/servidor. Es incluso más complejo ya que tenemos en una misma red coexistiendo un conjunto de servidores y clientes.
Inicialmente el proceso era muy sencillo, el cliente hacía una petición al servidor y éste le devolvía un archivo que el software del cliente tenía que interpretar. Pero los propietarios de los servidores empezaron a querer a realizar tareas más complejas. Querían disponer de capacidad cliente/servidor completa, que el cliente pudiera consultar en una base de datos, añadir datos, enviar información al servidor, etc.
Los exploradores web representaron un gran avance, permitieron que la información contenida en servidores se pudiera ver en cualquier tipo de computadora sin efectuar ninguna modificación. Los primeros eran bastante primitivos y se colapsaban rápidamente. No eran muy interactivos y sobrecargaban al servidor y a la propia red Internet, ya que cada vez que necesitaban algo que requería programación era necesario devolver la información al servidor para que éste la procesara. El explorador era un programa que mostraba información y no podía realizar ninguna otra tarea.
Para solucionar esto se mejoraron los estándares gráficos para poder visualizar vídeos y animaciones dentro de los navegadores. El resto del problema se resolvió permitiendo ejecutar programas del lado del cliente con el control del explorador, es decir, es programación del lado del cliente.
Programación del lado del cliente
El diseño inicial de la Web, basado en una arquitectura servidor/explorador, permitía contenido interactivo pero proporcionado sólo por el servidor. El servidor generaba páginas estáticas que el navegador del cliente interpretaba.El lenguaje HTML contiene una serie de mecanismos para crear formularios: cuadros de textos, casillas de verificación, botones de opción, listas normales y desplegables, además de un botón para enviar datos y otro para borrarlos. El envío de datos se lleva a cabo mediante una interfaz CGI situada en el servidor. El texto incorporado en el envío le proporciona información a la interfaz CGI sobre lo que tiene que hacer. Lo normal es ejecutar un programa situado en un directorio llamado cgi – bin. Estos programas se pueden escribir en casi cualquier lenguaje, aunque normalmente se utiliza Perl o Phyton. Con CGI se puede hacer casi de todo.
Pero CGI tiene algunos inconvenientes: mantener un sitio web basado en programas CGI puede resultar complicado, además el tiempo de respuesta de un programa CGI depende de la cantidad de datos a enviar, la carga del servidor y de la red Internet.
La solución a este problema es la programación del lado del cliente. Normalmente los ordenadores que tenemos en casa son bastante potentes. Con HTML estático los ordenadores se limitan a esperar a que el servidor envíe la página solicitada. La programación del lado del cliente hace que el navegador realice el máximo trabajo posible, de esta forma la navegación es más rápida e interactiva.
Plug–ins
Un plug–in es un mecanismo mediante el cual se añade alguna funcionalidad al explorador descargando un fragmento de código que se inserta en el lugar apropiado dentro del explorador, haciendo que tenga un nuevo comportamiento y sea más potente y rápido. Pero la realización de un plug–in no resulta trivial. Un plug–in permite desarrollar extensiones y añadirlas a un navegador sin necesitar un permiso por parte del propietario del navegador. Son como una puerta trasera que permite desarrollar nuevos lenguajes del lado del cliente.
Lenguajes de script
Los plug-ins dieron lugar a los lenguajes de script. Con un lenguaje de script, el código fuente de un programa del lado del cliente se integra directamente dentro de la página HTML y el plug-in que se encarga de interpretar ese lenguaje se activa de manera automática en el momento de visualizar la página web. Los lenguajes de script son normalmente fáciles de comprender y como se integran dentro del código HTML las páginas se cargan de manera rápida. La desventaja es que el código queda expuesto y puede verlo cualquiera.
Uno de los lenguajes de script que soportan los exploradores web sin necesidad de plug-ins es JavaScript, no tiene nada que ver con Java y el nombre se aprovecha del impulso inicial de marketing de Java. El problema es que cada navegador implementaba JavaScript de manera distinta. La estandarización de JavaScript mediante el lenguaje estándar EMACScript resolvió en parte el problema, pero los exploradores web tardaron bastante en adoptar el estándar. Para realizar un programa en JavaScript, hay que utilizar un mínimo común denominador para que el programa pueda ser interpretado de manera correcta por los diferentes navegadores. Otro problema que nos encontramos es que es muy difícil depurar y corregir errores (Google con Gmail tuvo muchísimos problemas).
Los lenguajes de script están diseñados para resolver determinados tipos de problemas, especialmente los relacionados con la creación de interfaces gráficas de usuario (GUI) más ricas e interactivas. Un lenguaje de script puede resolver el 80% de los problemas que nos podemos encontrar en la programación del lado del cliente. Si tenemos que resolver un problema que esté dentro de ese 80%, los lenguajes de script permiten realizar creaciones fáciles y rápidas. Si necesitamos una solución más compleja, tendremos que recurrir a un lenguaje como Java.
Java
¿Qué pasa con el 20% de problemas que no se pueden resolver mediante un lenguaje del lado del cliente, los realmente difíciles de resolver? La respuesta a esta pregunta puede ser Java. Se trata de un lenguaje muy potente que es multiplataforma, internacional y seguro. Este lenguaje está continuamente siendo ampliado para añadir nuevas funcionalidades y bibliotecas que permiten gestionar problemas que no pueden ser tratados con la programación tradicional como, por ejemplo, la concurrencia, acceso a base de datos, programación en red y distribuida. Java resuelve los problemas del lado del cliente mediante applets y Java Web Star.
Un applet es un mini – programa que sólo puede ejecutarse en un navegador. El applet se descarga como parte de la página web y cuando se activa se ejecuta un programa. Así podemos distribuir automáticamente el software del cliente desde el servidor en el momento en que el usuario necesita el software del cliente y no antes. De esta forma el usuario obtiene la última versión del software del cliente sin necesidad de complejas instalaciones. Un applet trabaja de la siguiente manera: el programador realizará un programa y éste podrá ejecutarse en cualquier navegador web que tenga instalado un intérprete de Java. Como Java es un lenguaje de programación completo, podemos llevar a cabo el máximo trabajo posible en el cliente, tanto antes de enviar una petición al servidor como después. Al realizarse gran parte del trabajo en el ordenador cliente se aumenta la velocidad, la capacidad de respuesta, y disminuye la carga de trabajo en el servidor y el tráfico de la red.
Alternativas
Los applets no han llegado a cumplir las expectativas creadas. Al principio parecía que los applets ilusionaban a todo el mundo, se podrían hacer tareas serias del lado del cliente, se mejorarían las respuestas de las aplicaciones basadas en Internet y reducirían el ancho de banda.
Aunque nos encontramos applets bastante interesantes en Internet, la gran migración que se esperaba no se produjo, varios factores la condicionaron: era necesario descargarse el entorno de ejecución Java Runtime Enviroment (JRE) y ocupaba 10MB, demasiado para un usuario medio. Además Microsoft no incluyó este entorno en su navegador, y sabemos que Internet Explorer es uno de los navegadores más utilizados. Por unos motivos u otros el uso de los applets no ha sido generalizado.
Sin embargo, los applets y la Java Web Star siguen siendo la solución más apropiada en algunos casos. En aquellos casos en los que tengamos control sobre máquinas de usuario, como en una gran empresa, resulta razonable distribuir y actualizar las aplicaciones cliente con esta tecnología, de este modo ahorraremos tiempo, dinero y esfuerzo, sobretodo cuando hay que hacer actualizaciones frecuentes.
Una tecnología bastante prometedora es Flex que permite crear equivalentes a los applets basados en Flash. El reproductor Flash Player está en el 98% de los navegadores.
.NET y C#
Hoy en día los grandes competidores de Java son .NET y C# de Microsoft. .NET es aproximadamente equivalente a la Java Virtual Machina (JVM es el intérprete de Java) y a las bibliotecas Java. C# tiene bastante parecido a Java. Se trata del mejor lenguaje que ha creado Microsoft, aunque contaban con la ventaja de estudiar lo que había funcionado y lo que no de Java. Los diseñadores de Java han estudiado con detalle C# y han introducido mejoras en Java con el lanzamiento de Java SE5.
El problema de .NET es que no es multiplataforma. Aunque el proyecto Mono (www.go-mono.com) dispone de una implementación parcial de .NET para Linux, hay que saber si la migración va a ser completa y no va a ser recortada por Microsoft. Por tanto es una apuesta arriesgada.
Redes Internet e Intranet
La Web es la solución para el problema de las arquitecturas cliente/servidor, por tanto, es la solución más adecuada para tratar el problema de las arquitecturas cliente/servidor internas a una empresa. Los problemas más habituales son la existencia de diferentes tipos de ordenadores, así como la dificultad de instalar nuevo software de cliente. Los navegadores y la programación del lado del cliente resuelven ambos problemas. Cuando se utiliza tecnología web para una red de información restringida a una empresa concreta, la arquitectura resultante es una intranet. La intranet proporciona un nivel de seguridad mucho mayor que Internet ya que podemos controlar físicamente el acceso a los diferentes equipos. Además al estar los usuarios familiarizados con el uso de navegadores, les resulta mucho más fácil el aprendizaje de los nuevos tipos de sistemas.
Si realizamos un programa que se ejecuta en Internet, no sabemos en qué plataforma se ejecutará y tendremos que tener especial cuidado a la hora de diseminar código erróneo. Debemos disponer por tanto de un lenguaje multiplataforma y seguro, como un lenguaje de script o Java.
Si nuestro programa se ejecuta en una intranet, la perspectiva es otra. No es extraño que trabajemos con máquinas Intel/Windows. En una intranet si escribimos código erróneo podemos corregirlo en el momento en que se descubra el error. Además puede que dispongamos de código heredado que haya estado siendo utilizado en plataformas más tradicionales, en las que es necesario realizar las actualizaciones de manera física. Esto último hace que se pierda mucho tiempo, por tanto, es uno de los motivos para empezar a utilizar exploradores, las actualizaciones en este caso, son transparentes y automáticas.
A la hora de resolver un problema debemos tener en cuenta qué restricciones afectan a este problema. Una vez analizadas, utilizaremos la solución más rápida en cada caso. Algunas veces resolveremos nuestro problema con un lenguaje de script y otras con Java.
Programación del lado del servidor
Cuando enviamos una petición a un servidor, ¿qué ocurre? Normalmente esta petición dice “envíame este archivo”. Nosotros recibimos el archivo y el explorador lo interpreta de manera correcta.
Algunas solicitudes pueden implicar una transacción de base de datos. Una operación común es hacer una búsqueda en una base de datos, una vez terminada la búsqueda, el servidor formatea los datos de manera correcta para que sean vistos en el ordenador del cliente. Otra operación es, por ejemplo, la realización de un pedido, esto supone agregar datos a una base de datos. Ambas operaciones son ejemplos de programación del lado del servidor. Tradicionalmente esta programación se llevaba a cabo con Perl, Phyton y C++, pero con el tiempo se han desarrollado sistemas más avanzados como los servidores web basados en Java. En estos sistemas todos los programas se realizan mediante Java, creando lo que se denomina servlets. Los servlets y sus descendientes, las páginas JSP, están siendo adoptados por las empresas que desarrollan sitios web. El motivo es que estas tecnologías eliminan el problema de tratar con diferentes tipos de navegadores. La programación del lado del servidor se trata en Think in Enterprise Java (www.mindview.net).
Java es un lenguaje de propósito general que permite resolver los mismos problemas que se pueden resolver con otros lenguajes. Pero tiene numerosas ventajas: portabilidad, programabilidad, robustez, amplia biblioteca en continua actualización y bibliotecas de otros fabricantes.
super tu blog, iré viendolo poco a poco, soy de ing industrial pero me gusta tener un hobbie y ese va ser aprender a programar.
ResponderEliminarSaludos
Ese hobbie está muy bien, te aconsejo que seas persistente y que no te desanimes con las adversidades. Espero que te ayude mi blog.
EliminarUn saludo y gracias.
Este comentario ha sido eliminado por el autor.
EliminarQ bueno que haya alguien que no es de Informatica y le guste programar...desgraciadamente el area de Informatica de muchas empresas esta lleno de Informaticos que NO SABEN programar haciendose cargo de proyectos como si supieran, ni que hablar de los Contadores que son Gerentes de Informática...y todos cobrando mejores sueldos que los programadores.
ResponderEliminarBienvenido a la tribu de los programadores/desarrolladores...
Muy buen blog!!!
ResponderEliminarmi enhorabuena.
Muchas gracias, espero que te ayude a aprender Java.
EliminarUn saludo.
:-)