CURSO PROGRAMACIÓN DE SERVIDORES WEB CON ACCESO A BASES DE DATOS (2ª Edición)

Curso Básico de Java

Este curso está dirigido principalmente a alumnos que no tengan conocimientos sobre Java (o tengan muy pocos), de forma que pretende ser una primera introducción a las características y conceptos fundamentales de dicho lenguaje. Debido a eso muchos de los conceptos simplemente se 'perfilarán' o se nombraran sin entrar en demasiados detalles sobre los mismos.
Por otro lado, el curso también podrá servir a usuarios con más conocimientos en el tema, puesto que dedica un capítulo entero al tratamiento de Threads (Hebras), que quizá sean una de las posibilidades más potentes de Java, pero que conllevan cierta dificultad en su programación y en su entendimiento.

Introducción

Historia de Java

Java surgió en 1991 cuando un grupo de ingenieros de Sun trataban de diseñar un nuevo lenguaje de programación destinado en principio a electrodomésticos (sencillo y capaz de generar código de tamaño reducido). El continuo cambio en las CPUs obligaba a actualizar los programas continuamente por lo que se pensó en desarrollar una herramienta que fuera independiente de la CPU. Así pues, idearon un lenguaje capaz de crear 'código neutro' independiente del electrodoméstico, que posteriormente era ejecutado en una hipotética 'máquina virtual', denominada Java Virtual Machine (JVM) que lo interpretaba y convertía a código particular de cada CPU. Por esa razón se eligió como primer lema para Java "Write Once, Run Everywhere" (escríbelo una vez, ejecútalo en cualquier lugar).

Tras tantos esfuerzos, ninguna empresa de electrodomésticos se interesó por Java, de modo que, a finales de 1995, comenzó a introducirse como lenguaje de programación para computadores pasando a ser una herramienta necesaria, puesto que Netscape Navigator 2.0 la introdujo como intérprete.

Desde ese momento Java ha evolucionado favorablemente, a principios de 1997 se publicó Java 1.1 y a finales de 1998 Java 1.2, con un crecimiento y mejora prodigiosos, pasando de los 12 paquetes iniciales a 23 paquetes en la versión 1.1 y a 59 paquetes en la versión 1.2 (denominada Java 2). Esta evolución ha continuado y actualmente JDK (Java Development Kit) se encuentra en su versión 1.5.0_06. En este curso se verá sólo la punta del iceberg en que se ha convertido.

La compañía Sun describe a Java como "simple, orientado a objetos, distribuido, interpretado, robusto, seguro, de arquitectura neutra, portable, de altas prestaciones, multitarea y dinámico". Lo bueno de esta definición es que todo lo que dice, que no es poco, es cierto, aunque en algunos aspectos mejorable.
Ir a Inicio

 

Programación Orientada a Objetos

Aunque el contenido de este curso se centra en Java, a continuación se describirán brevemente algunos conceptos que deberían tenerse claros antes de trabajar con un lenguaje orientado a objetos como el que nos ocupa.

Se conoce como Clase a una estructura que contiene atributos y métodos. Dichos atributos serán variables correspondientes a características de la misma y los métodos serán funciones que realizarán operaciones relacionadas con la clase (generalmente actuarán sobre los atributos de ésta).
Un Objeto es una instancia concreta de una clase, cada uno será diferente de los demás, aunque sean instancias de la misma clase.
Se podría entender entonces que una clase equivaldría a un 'patrón' a partir del que podemos crear objetos.

Se dice que una clase Encapsula datos, ya que desde fuera de la misma, por lo general, no es posible trabajar con sus atributos si no es por medio de los métodos que ofrece.
Otro concepto fundamental es la Herencia, consistente en la posibilidad de definir clases a partir de otras en el sentido de que tendrán muchos (o todos) los atributos y métodos comunes. Estas clases se conocen comunmente como padre (la que se crea primero) e hija (la clase creada a partir de otra). Esta última a su vez, podría añadir nuevos atributos o métodos (que no tendría el padre) y ser además padre de otras clases.
Por último, se conoce como Polimorfismo a la posibilidad que tiene un objeto de 'comportarse' de distintas formas usando el mismo método. Es decir, el objeto 'decidirá' qué método usar (o cómo usarlo) en función de los parámetros que se den en la llamada al mismo o en función de su clase.

Un programa orientado a objetos (POO) se basa en la creación de una serie de clases que 'encajan' perfectamente y que implementan la funcionalidad que se pretente que tenga el programa. Dichas clases, deberán trabajar con datos y métodos y comunicarse entre ellas para obtener los resultados deseados.

Para ampliar estos conceptos se puede acceder, por ejemplo, a POO en Wikipedia.

Orientación a Objetos en Java

El funcionamiento general de un POO se especifica en una clase principal la cual contiene un método fundamental que es el primero que se ejecuta, en Java es la función main().

Al programar en Java no se parte de cero, ya que por defecto, en los paquetes de desarrollo (JDK), hay un gran número de clases predefinidas, en las que se 'apoyan' las que nosotros creamos.

En Java, las clases son estructuras (definidas con class) que contienen una serie de variables (atributos) y un conjunto de funciones (métodos).

La encapsulación viene representada en este lenguaje por los diferentes permisos de acceso o visibilidad que podemos asignar tanto a las clases, como a sus atributos y métodos, pudiendo 'ocultarlos' al resto de las clases si lo deseamos.

La herencia se usa muy a menudo en el desarrollo en Java, para ello se utiliza la palabra clave extends entre dos clases (claseH extends claseP), dónde la primera será hija de la otra y heredará todos sus atributos y métodos. Además, dicha clase hija (también llamada derivada) podrá añadir nuevos atributos y métodos o redefinir los heredados. En Java una clase sólo puede heredar de otra (solo puede tener un padre), es lo que se denomina herencia simple.

El polimorfismo también está presente en Java, pudiendo encontrarse dos tipos de polimorfismo. Por un lado existe la posibilidad de definir varias funciones con el mismo nombre dentro de la misma clase (funciones sobrecargadas). La resolución de las mismas tiene que ver con la relación que se establece entre la llamada a un método y el código que efectivamente se asocia con dicha llamada. Esta relación se llama vinculación o binding y puede ser temprana, en tiempo de compilación, o tardía, en tiempo de ejecución. La vinculación temprana es la más eficiente y es la que se da normalmente con funciones normales o sobrecargadas. La vinculación tardía es la que se utiliza con funciones redefinidas (tras heredar).
Por otro lado, es posible usar un objeto de la subclase como si fuera uno de la superclase (usar los métodos y variables propios de la superclase) y viceversa (aunque con restricciones en el segundo caso).

Además de clases, Java proporciona interfaces. Un interface es un conjunto de declaraciones de métodos sin contenido (sin código). Si una clase implementa (implements) un interface, debe definir todas las funciones especificadas en él (crear el código de las mismas). Una clase puede implementar ninguno, uno, o varios interfaces. Un interface sí soporta la herencia múltiple de varios interfaces.
Ir a Inicio

 

Características Generales de Java

En esta sección se describirán una serie de conceptos relacionados con la programación en Java, desde la forma habitual de desarrollar los programas, a las normas (no establecidas) de programación, pasando por ciertas particularidades de este lenguaje.

Desarrollo en Java

Existen distintos programas comerciales que permiten desarrollar código Java, aunque lo más común es utilizar el Java Development Kit (JDK) (incluido en el J2SE) que distribuye gratuitamente SUN. Se trata de un conjunto de programas y librerías de clases (paquetes) que permiten desarrollar, compilar y ejecutar programas en Java.
Además, incorporan la posibilidad de ejecutar parcialmente el programa, deteniendo la ejecución en el punto deseado y estudiando en cada momento el valor de cada una de las variables (con el denominado Debugger).
Del mismo modo, existe también una versión reducida del JDK, denominada JRE (Java Runtime Environment) destinada únicamente a ejecutar código Java(no permite compilar).

Los IDEs (Integrated Development Environment) o entornos de desarrollo integrados, son aplicaciones que permiten escribir el código Java, compilarlo y ejecutarlo de manera centralizada. Estos entornos permiten desarrollar las aplicaciones de forma mucho más rápida, incorporando en muchos casos librerías con componentes ya implementados, los cuales se incorporan al proyecto o programa de forma sencilla. Como inconvenientes se pueden señalar algunos fallos de compatibilidad entre plataformas, y ficheros resultantes de mayor tamaño que los basados en clases estándar.
Existen varios IDEs comerciales como son Visual J++ (Microsoft) o JBuilder (Borland). Un ejemplo de IDE de libre distribución es Eclipse.

Aparte de lo mencionado, hay otras cuestiones a tener en cuenta a la hora de desarrollar un programa en Java:

Particularidades de Java

'Normas' de Programación

Este apartado no pretende ser una lista de normas estrictas, sino que describe un conjunto de 'costumbres' que, cuando te conviertes en programador de Java, vas adoptando poco a poco puesto que ayudan a entender mejor los programas y a integrarte mejor en la gran comunidad de programadores que existe.

Ir a Inicio

 

 

Pequeño Manual de Programación en Java

Este capítulo ofrece una guia de referencia inicial de los tipos posibles, palabras reservadas, operadores y estructuras de control que podemos usar para programar en Java. Además comenta la forma de definir de clases, variables, métodos, packages e interfaces, revisando algunas de las posibilidades que nos ofrece este lenguaje, así como sus peculiaridades o restricciones.

Conceptos iniciales

En Java se tienen como tipos primitivos: char, byte, short, int, long, float, double, boolean.
Hay que tener cuidado con el tipo boolean que toma como valores true o false los cuales no son equivalentes a 1 y 0 (como en C++).

Para definir e inicializar una variable, basta con seguir el esquema:

TipoClase nombreVariable;     //definición
nombreVariable = valor | new Clase();     //inicialización

Los nombres de las variables deberán contener solo caracteres alfanuméricos o '_' y no ser iguales a ninguna palabra reservada.
Una variable podrá ser un tipo primitivo o un tipo referencia (array u objeto). En los dos últimos casos deberá inicializarse usando la instrucción new.

Una cadena en Java es un objeto del tipo String, pero aún así puede ser tratado como un tipo primitivo. Por ejemplo, para inicializar una cadena no es necesario hacer un new, sino que se pueden crear como una variable normal:
Ej. String micadena = "cadena de prueba"; // creada como tipo primitivo
Aunque si se desea, se puede inicializar con new:
Ej. String micadena = new String("cadena de prueba"); // creada como objeto

Los arrays igualmente son objetos y se deben crear según la estructura:

TipoClase [ ]..[ ] nombreArray;     //definición
nombreArray = new TipoClase[num_elems1][num_elems2]...[num_elemsN];     //inicialización

Teniendo en cuenta que si se trata de un array de objetos de una clase, deberemos recorrerlos inicializando cada uno de ellos usando el constructor de su clase (haciendo un new para cada uno).
Al igual que en C/C++, los arrays tienen índices entre 0 y (número de elementos-1).

El valor nulo en Java es null y se utiliza para liberar la memoria de las variables asignándoselo como si fuera un valor.

En Java, las conversiones entre tipos se hacen de forma automática si pasamos de un tipo a otro de más precisión (de int a long, de float a double). Entre clases ocurre igual si queremos convertir un objeto de una subclase (hijo) en otro de su superclase (padre), ya que la subclase tendrá todos los métodos y variables de la superclase (gracias la herencia).
En los demás casos es posible 'forzar' conversiones entre tipos y clases (siempre dentro de unos márgenes), es lo que se conoce como casting. La forma de convertir un tipo o clase en otro sería:

TipoClase1 variable1    
TipoClase2 variable2    
variable1 = (TipoClase1) variable2;     // en lugar de variable2 podríamos tener una expresión

Como se ha dicho se deben respetar ciertas normas, como que algunos tipos no se pueden convertir mediante casting en otros (Ej. un boolean no puede convertirse en int) o que, en el caso de las clases, para convertir un objeto de una clase A en uno de una clase B, ambas deben estar relacionadas mediante herencia. Si A es superclase de B, deberemos hacer un casting explícito como se indica arriba y podría dar errores ya que la clase A podría no tener ciertos métodos o variables de la clase B.
En cualquier caso, también existen métodos para realizar conversiones entre tipos poco compatibles en principio (como pueden ser cadenas y números)

Operadores

Los operadores aritméticos son + , - , * , / y % (resto de la división).
Los operadores relacionales son > , < , >= , <= , == (igual) y != (distinto).
Los operadores lógicos son && (and) , || (or) , ! (negación).
Otros operadores: ++ (incremento) , -- (decremento) , = (asignación).

El operador + también se usa para la concatenación de cadenas.

El operador instanceof devuelve un valor booleano y se usa para saber si una variable es de una determinada clase.
Ej. mivar instanceof UnaClase //devuelve true si miVar es de la clase UnaClase .

Consideraciones

Para finalizar cada sentencia se debe poner un ' ; '.
Los Comentarios en Java se marcan con ' // ' al principio de cada línea a comentar o con ' /* ' y ' */ ' delimitando un conjunto de líneas de comentario.
Para determinar un bloque de código se usan como delimitadores ' { ' como inicio y ' } ' como fin del bloque.
Java es sensible a mayúsculas y minúsculas, es decir, dos nombres serán iguales solo si coinciden todas sus letras y están en mayúsculas/minúsculas las mismas letras: Ej. 'Tierra' es diferente de 'tierra' y ambas son diferentes de 'TIERRA'.
Ir a Inicio

 

Palabras Reservadas

A continuación se muestra una lista con las palabras reservadas de Java, sin ningún tipo de explicación, sino únicamente para tener constancia de su existencia. Si se desea más información, se recomienda consultar la documentación existente al respecto.

abstract
boolean
break
byte
case
catch
char
class
continue
default
do
double

else
extends
final
finally
float
for
if
implements
import
instanceof
int
interface

long
native
new
null
package
private
protected
public
return
short
static
super

switch
synchronized
this
throw
throws
transient
try
void
volatile
while


Ir a Inicio

 

Estructuras de Control

En este apartado se comentarán brevemente las estructuras de control (para determinar el flujo del programa) que se pueden usar en Java.

Condicionales:

if (ExpresionBooleana) 
{
    sentencias;
}

  if (ExpresionBooleana) 
  {
       sentencias1;
  } else {
       sentencias2;
  }

  if (ExpresionBooleana1) { 
       sentencias1;   
   } else if (ExpresionBooleana2) { 
       sentencias2;   
   } else if (ExpresionBooleana3) { 
       sentencias3;
   ...
   } else {
       sentenciasN;
   }   

  switch (Expresion) {
       case valor1: sentencias1; break; 
       case valor2: sentencias2; break; 
       case valor3: sentencias3; break; 
       ...
       [default: sentenciasN;]
   }

Bucles:

while (ExpresionBooleana) { 
    sentencias;
}

  for (Inicializacion; ExpresionBooleana; Incremento) { 
      sentencias;
  }

  do {
      sentencias;
  } while (ExpresionBooleana);  

Como nota indicar que en el bucle for, la inicialización solo se hace la primera vez y el incremento una vez terminadas las sentencias. Además señalar que es posible hacer varias inicializaciones y/o varios incrementos, en ambos casos separando por comas las diferentes expresiones.


Excepciones:

La excepciones son errores 'controlados' y 'recuperables' que surgen durante la ejecución de un programa. En Java se pueden 'capturar' y actuar en consecuencia, para ello se usa la estructura try...catch, en la cual el código en el bloque try es supervisado, de forma que si ocurre una excepción, se pasará a hacer las acciones definidas en el bloque catch correspondiente a dicho error (habrá uno por cada error a controlar). Existe también un bloque finally que se ejecuta ante cualquier tipo de error (no controlado anteriormente en un catch).

try
    sentenciasT;
} catch (ClaseExcepcion1 nombreVariableExcepcion1) { 
    sentenciasE1;
} catch (ClaseExcepcion2 nombreVariableExcepcion2) { 
    sentenciasE2;
}     ...
finally
    sentenciasF;
}



Para interrumpir el flujo normal del programa y modificarlo brúscamente, se usan las instrucciones:

break, sale del bloque, ya sea una condición o un bucle, sin ejecutar ninguna sentencia más.
continue, útil solo en bucles, en los que pasa a la siguiente iteración del mismo.
return, sale de un método de manera inmediata. Suele usarse junto con un valor que es el que devuelve el método.
Ir a Inicio

 

Clases y Objetos

En este apartado se describirá con brevedad la forma común de definir una clase, los atributos y métodos, así como las diferentes opciones con las que se pueden crear y la instanciación de un objeto de dicha clase.

En general, la forma usual de definir una clase es:

[visibilidad] [modificador] class NombreClase [herencia] [implementación] {
   // definición de variables y métodos
   ...
}

Dónde:

La visibilidad es el ámbito en el que es conocida una clase, es decir, determina desde qué paquetes o clases es posible crear objetos de dicha clase y trabajar con ellos.
Con public la clase podrá ser vista desde cualquier otro paquete (o clase). Mientras que con package la clase solo podrá ser vista dentro el mismo paquete. Es la opción por defecto si no se especifica nada.

Se puede usar el modificador abstract para definir un tipo de clases de las que no se pueden crear objetos. Su utilidad es que otras clases deriven o hereden (extends) de ella, proporcionándoles un marco o modelo a seguir y una serie de métodos de utilidad general.
Es posible tener métodos completamente definidos dentro de una clase abstract y además tener métodos abstract.

La herencia, como ya se comentó anteriormente, es simple (solo entre una clase hija y otra padre) y se identifica usando la palabra reservada extends más la clase de la que se hereda (clase padre). Ej. class Circulo extends Figura. Debemos recordar que mediante la herencia conseguimos que la clase hijo disponga de las mismas variables y funciones que la clase padre, las cuales puede redefinir. Además, dicha clase podrá tener otras variables y métodos propios.

La implementación se refiere a las posibilidades que ofrecen los interfaces, ya comentados en un apartado anterior. Para definir una clase que implemente un interfaz ya definido se usa la palabra reservada implements, junto con el nombre del interfaz (o interfaces separados por comas) a implementar. Ej. class Cuadrado implements InterfazFigura. Recordamos nuevamente que un interfaz es un conjunto de declaraciones de funciones (funciones vacías, se podría decir) agrupadas bajo un nombre al igual que una clase.

La definición de un interfaz sería:

[visibilidad] interface NombreInterfaz [herencia] [implementación] {
   // definición de funciones
   ...
}

Al igual que una clase, un interfaz podrá heredar (extends) de otro u otros (cosa que no pueden hacer las clases), sin más que separarlos por comas. Del mismo modo, también podrá implementar (implements) a otro u otros.
Los interfaces se suelen definir en ficheros con su nombre y solo podrán ser public o package (al igual que las clases).

Atributos (variables miembro)

Para declarar un atributo de una clase, basta con declararlo como cualquier variable dentro del cuerpo de dicha clase, la forma sería:

[visibilidad] TipoClase nombreAtributo;

dónde la visibilidad será public,package,private o protected, siendo public y package similares a los comentados para la definición de clases y private y protected sirven para asegurar que los atributos solo se puedan acceder desde dentro de la misma clase (desde los métodos de ésta).

Existe un tipo de variables llamadas variables de clase que se definen anteponiendo la palabra reservada static y que tienen como particularidad que son variables comunes a todos los objetos, pero que solo tienen sentido y solo se manejan a nivel de clase. Ej. un contador de objetos creados de una clase.
Suelen usarse para definir constantes que puedan usar todos los objetos de la clase.

Por último hablaremos de las variables declaradas con la palabra clave final, que son aquellas que no pueden cambiar su valor a lo largo de la ejecución (equivalen a constantes).

Métodos (funciones)

Los métodos son funciones definidas dentro de una clase, la forma general de declararlos es:

[visibilidad] [modificador] ClaseTipo_ValorRetorno nombreMétodo ( [ClaseTipo_parametro1 nombreParametro1, ..., ClaseTipo_parametroN nombreParametroN] ) {
    sentencias;
}

dónde la visibilidad será nuevamente public,package,private o protected, con el mismo significado que en el caso de los atributos.

Los parámetros del método se pasan siempre por valor, es decir, aunque se modifiquen dentro del mismo, su valor no cambiará fuera de él. Cuando se pasan referencias, ya sean arrays u objetos, la referencia tampoco podrá cambiar, pero sus valores (los elementos del array o los atributos del objeto) si que podrán hacerlo.
Como se puede ver los parámetros de los métodos son optativos, de modo que si no tienen, basta con dejar los paréntesis vacíos.

Los métodos también se pueden declarar con el modificador abstract, al igual que las clases. Su utilidad es definir un modelo de método para las clases que hereden de aquella en la que estén definidos. Si una clase tiene un método abstract, entonces la clase deberá ser abstract.

Los métodos tienen visibilidad directa de las variables miembro de la clase en la que están definidos, es decir, pueden hacer referencia a ellas.
Dentro de los métodos se pueden declarar variables locales que tendrán como ámbito dicho método (al finalizar las eliminará el recolector de basura).

Para hacer referencia a algún elemento de la clase misma en la que se encuentra el método se utiliza la palabra this. Del mismo modo, para hacer referencia a una variable o método de la clase padre de en la que esté el método (en caso de que la haya), se utiliza la palabra super.

El valor de retorno es único y se devolverá usando la instrucción return dentro del cuerpo del método, finalizando el mismo de manera inmediata.

Al igual que las variables, también existen métodos de clase, que se declaran igualmente anteponiendo static y que nuevamente solo tienen sentido a nivel de clase. Ej. función que incrementa el número de objetos creados de una clase.

Existe un tipo de métodos que deben definirse para todas las clases (para evitar errores), son los llamados constructores y son funciones que tienen como objetivo inicializar los atributos de un objeto a la hora de crearlo. Deben tener el mismo nombre que la clase en la que se definen y no tienen valor de retorno. Puede haber varios, aunque con distinto número de parámetros. Los constructores son llamados cuando inicializamos un objeto con la instrucción new.
Los constructores pueden ser llamados (con el mismo nombre de la clase y los parámetros pertinentes) desde dentro de otros constructores o métodos de clase (incluso de clases hijas usando super), pero no pueden ser llamados desde otro tipo de métodos.
Si no se define ninguno, Java crea uno automáticamente en el que inicializa las variables con sus valores por defecto.

Es posible definir varios métodos con el mismo nombre dentro de la misma clase, aunque con distinto número y/o tipo de parámetros, es lo que se conoce como sobrecarga de un método y con ella se implementa el anteriormente citado polimorfismo en Java.
Del mismo modo, es posible sobreescribir los métodos heredados en la clase hija, es la llamada redefinición.

Objetos

Los objetos son instancias de las clases, como ya se comentó, cada objeto es diferente de los demás, aunque sean de la misma clase, por lo que tendrá valores propios para sus atributos que determinarán un estado diferente de los demás.
Para crear un objeto basta con declararlo como de una clase y después inicializarlo (llamando al constructor), por ejemplo:

MiClase miObjeto;
miObjeto = new MiClase();


Para acceder a los atributos y los métodos que tiene el objeto (los que ofrece la clase y siempre que lo permita la visibilidad), bastará con usar el operador ' . ', por ejemplo:

miObjeto.x = 10;
miObjeto.calcularY(20);

Ir a Inicio

 

Packages

Para concluir el capítulo, en esta sección hablaremos brevemente de los packages, su gestión y su utilidad en Java.
Un package es un conjunto de clases relacionadas de alguna forma (es similar a una librería en otros lenguajes). Todas las clases del mismo package deberán estar en el mismo directorio, de forma que se establece una relación directa entre package y directorio.
Un usuario podrá crear sus propios packages escribiendo al principio del fichero con la clase a incluir la instrucción:

package nombrePackage;

Generalmente, estos nombres estarán en minúscula y contendrán varias palabras separadas por puntos, que se corresponden usualmente con la estructura de directorios en la que se encuentra el package, a partir del path que determina la variable CLASSPATH (de la que se hablará más adelante).

Para utilizar las clases incluidas en un package (las públicas), se usa la instrucción:

import nombrePackage.[*|NombreClase.class];

Pudiendo elegir una clase concreta o todas las clases del package con ' * '.
Esto se debe hacer explícitamente para todos los que queramos importar, aunque sean sub-packages de otros, ya que esto último no se hace de manera automática.
Ir a Inicio

 

Clases Útiles

Este capítulo enumera y describe en pocas líneas una serie de clases que seguro resultarán de utilidad al alumno. Algunas de ellas se comentarán con más profundidad en capítulos posteriores.

Wrappers Son clases diseñadas para complementar los tipos primitivos (ya que no son objetos), añadiendo al valor en si (definido como una variable miembro de la clase) una serie de métodos para facilitar su uso. Los wrappers definidos son Byte, Short, Integer, Long, Float y Double
java.lang.Math Proporciona métodos para realizar operaciones matemáticas, así como algunas constantes como PI o E.
Ej. Math.random()
Colecciones Una colección no es más que un conjunto de objetos que se agrupan, cualquier colección se identifica por el interfaz Collection.
Son colecciones las clases java.util.Vector, java.util.HashSet, ... y también son colecciones los interfaces java.util.List, java.util.Map, ...

Collections

- Collection: define métodos para tratar una colección genérica de elementos.
- Set: Colección de elementos que no admite repetición.
- SortedSet: es un Set ordenado según un criterio establecido.
- List: admite elementos repetidos y mantiene el orden de inserción.
- Map: Conjunto de pares, clave/valor, sin repetición de claves.
- SortedMap: es un Map ordenado según un criterio establecido.
- Iterator: Interfaz de soporte utilizado para recorrer una colección y para borrar elementos.
- ListIterator: interfaz de soporte que permite recorrer List en ambos sentidos.
- Comparable: interfaz de soporte que declara el método compareTo() que permite ordenar las diferentes colecciones según un orden natural.
- Comparator: interfaz de soporte que declara el método compare() y se utiliza en lugar de Comparable: cuando se desea ordenar objetos no estándar o sustituir a dicha interface.
java.awt AWT son las siglas de Abstract Windows Toolkit y es la parte de Java que se permite construir interfaces gráficas. Para construir una interfaz, se necesitan al menos tres elementos, un contenedor, unos componentes y un modelo de eventos.
El contenedor es la ventana o parte de la ventana donde se sitúan los componentes.
Los componentes son menús, botones, barras de desplazamiento, cajas y áreas de texto, etc.
El modelo de eventos es la herramienta que nos permite controlar las acciones del usuario sobre los componentes, de modo que cada vez que el usuario realiza una acción se produce un evento y AWT crea un objeto de la clase de evento correspondiente derivada de AWTEvent. El evento será posteriormente gestionado por un objeto que debe conocer el componente que lo ha recibido.
De forma que se diferencian dos tipos básicos de objetos, los que reciben los eventos (event source) y los que manejan los eventos (event listener).
Los objetos event source deben 'registrar' los objetos que habrán de gestionar sus eventos, mientras que los objetos event listener deben implementar los métodos adecuados para manejar un cada evento. La forma más sencilla de hacerlo es implementar el interfaz Listener de forma adecuada.
Threads Los threads (hebras) o hilos de ejecución permiten organizar los recursos del ordenador de forma que pueda haber varios programas o varias partes del mismo programa ejecutándose en paralelo. Un hilo de ejecución realizará las acciones indicadas en el método run().
(esta clase se estudiará con más detalle en capítulos posteriores del curso)
Exceptions Las excepciones y su tratamiento (forma de capturarlas, respuesta a las mismas), ya se comentaron en el capítulo anterior, por lo que no se entrará en grandes detalles. Solo cabe mencionar que una excepción no es igual que un error, ya que que las excepciones son recuperables, mientras que los errores no lo son, de hecho las excepciones están organizadas en una jerarquía que parte de la clase java.lang.Exception mientras que los errores tipificados están organizados a partir de la clase java.lang.Error.
También hay que comentar la posibilidad que tiene el programador de lanzar excepciones (throw) cuando él lo desee (al detectar un dato en formato incorrecto para el programa, por ejemplo). Ésto se haría mediante objetos de dicha clase Exception

Para ampliar los conceptos acerca de cualquiera de estas clases, así como de otras muchas que pudieran resultar de utilidad para el programador, se recomienda revisar la documentación que ofrece SUN via internet, por ejemplo:

Documentación sobre todas las distribuciones de Java
Documentación sobre API de JDK
Ir a Inicio

 

Compilar y Ejecutar

Antes de nada cabría definir muy brevemente lo que se entiende por compilar y ejecutar.
Compilar un programa consiste en convertir dicho programa, que no será más que código (texto) escrito en un lenguaje de programación y guardado en uno o varios ficheros, en código máquina que pueda interpretar la CPU. En el caso de Java, se convierte en realidad en el llamado bytecode (fichero '.class') que es el código que deberá interpretar la máquina virtual de Java (JVM) que tengamos instalada en nuestro sistema operativo. Debemos recordar que una de las características que hacen potente a Java es la posibilidad de ejecutar el mismo bytecode en diferentes CPUS y sistemas operativos.
Durante la compilación se identifican algunos posibles errores que pueda haber en el programa, como errores sintácticos (algo mal escrito según la sintaxis del lenguaje de programación), variables o métodos no declarados, fallos en la llamada a métodos, etc. Para que compile correctamente un programa no deberá haber ningún error.
Ejecutar un programa consiste en interpretar el código máquina generado en la compilación, dando como resultado la aplicación que hayamos programado. Java nuevamente se desmarca del resto (que genera ficheros ejecutables ('.exe' en windows)), puesto que tiene un comando para ejecutar el programa que lanza la máquina virtual que interpreta el bytecode.
Durante la ejecución también pueden surgir errores, los cuales no pueden ser detectados en tiempo de compilación, pues generalmente se trata de referencias en memoria no controladas (intentos de acceso fuera de los límites de un array, por ejemplo) o valores incorrectos, y estos accesos o asignaciones solo se producen durante la ejecución.

Centrándonos ya en nuestro tema, el primer paso sería instalar los paquetes necesarios para compilar un programa en Java (Java Development Kit (JDK)), posteriormente deberemos comprobar que la variable del sistema PATH contiene la rutas de los programas de Java y la variable CLASSPATH tiene las rutas de los paquetes del API de la JDK (las distribuciones para Windows suelen actualizar dichas variables de manera automática).

Compilación de un Programa

El comando para compilar desde la línea de comandos de cualquier sistema operativo (ventana de MS-DOS o command en Windows y terminal en Linux) es:

javac [opciones] fichero.java

Las opciones se pueden ver si se ejecuta sin argumentos. De todas ellas, la más utilizada y a su vez la que más complicaciones crea a los nuevos programadores o usuarios es la opción -classpath. Esta opción se utiliza para incluir los directorios donde se deben buscar las clases necesarias para compilar el programa deseado. Las rutas de dichos directorios se deben escribir separadas por ' ; ' en Windows o por ' , ' en Linux. Cualquier clase necesaria que no esté incluida en esta lista provocará un error del compilador.

En el caso más sencillo, supongamos que queremos compilar un fichero file.java que pertenece al paquete mipaquete, y el directorio actual es c:\mipaquete la sentencia para compilar este fichero sería:

javac -classpath . file.java

Pero supongamos que el fichero file.java contiene una sentencia import otropaquete.mio.MiClase y el fichero de esta clase, MiClase.java, está en el directorio c:\otropaquete\mio\MiClase. En este caso la sentencia anterior daría un error por que no puede encontrar la clase MiClase y la sentencia correcta sería:

javac -classpath .;..\otropaquete\mio file.java

Otra posible situación sería que la clase que hemos incluído no esté en nuestro directorio sino que se trate de una clase incluída en un fichero '.jar'. Para estos casos el classpath debe incluir la dirección y nombre completo del fichero y debe ponerse el primero. Por ejemplo:

javac -classpath direccion.ficherojar.jar;.;..\otropaquete\mio file.java

Ejecución de un Programa

Es importante señalar que para que un programa en Java 'haga algo', es necesario definir dentro de una clase pública (clase principal) un método main. En el comenzará todo el proceso del programa y generalmente tendrá la forma:

public static void main (String[] args) {

   sentencias_programa_principal;

}

Para ejecutar un programa Java el comando que se debe utilizar es:

java [opciones] programa

El programa debe ser el nombre de un fichero '.class' (sin poner la extensión) que incluya la función main.
Las opciones disponibles se pueden ver si se ejecuta el comando sin argumentos al igual que al compilar.
De nuevo, la opción más engorrosa es -classpath, que sigue las mismas reglas que para el comando anterior.

Como ejemplo podemos ver las sentencias para ejecutar los programas compilados en los ejemplos anteriores:

java -classpath . file
java -classpath .;..\otropaquete\mio file
java -classpath direccion.ficherojar.jar;.;..\otropaquete\mio file

Ir a Inicio

 

 

Primeros Ejemplos

En este capítulo, se mostrará el código de varios ejemplos sencillos (clásicos en el aprendizaje de cualquier lenguaje de programación), comentados para su mejor comprensión, y se pedirá al alumno que los pruebe, arregle el que tiene errores y que intente comprenderlos.

Hola Mundo Java!

//fichero: HelloJava0.java
public class HelloJava0 {

 // función principal del programa
 public static void main(String[] args) {
  HelloJava0 helloJava = new HelloJava0(); //creamos un objeto de la clase
  helloJava.printHello(); //usamos el método printHello de la clase
 }

 // método miembro de la clase
 public void printHello(){
   System.err.println("Hola, Java!");
 }

}

Compila y ejecuta el ejemplo anterior (copiándolo a un fichero) para ver cómo funciona.

Ir a Inicio

 

 

Algo Más Complicadillo

//fichero: HelloJava1.java
public class HelloJava1 extends javax.swing.JComponent {
// se trata de una clase que es un JComponent.

  public static void main(String[] args) {
   javax.swing.JFrame f = new javax.swing.JFrame("HelloJava1");
   f.setSize(300, 300);
   // añade un componente al contenedor.
   f.getContentPane().add(new HelloJava1( ));
   f.setVisible(true);
  }

  public void paintComponent(java.awt.Graphics g) {
   // repinta el objeto cada vez que es necesario
   g.drawString("Hola, Java!", 125, 95);
  }
}

//fichero: HelloJava2.java
// import más complejo
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class HelloJava2 extends JComponent implements MouseMotionListener {
// es una clase componente que va a responder a los eventos relacionados con el movimiento del ratón

  // Coordenadas del mensaje
  int messageX = 125, messageY = 95;
  String theMessage;

  public HelloJava2(String message) {
   //constructor
   theMessage = message;
   // establece que el objeto que va a manejar los eventos del ratón va a ser él mismo.
   addMouseMotionListener(this);
  }

  public void paintComponent(Graphics g) {
   g.drawString(theMessage, messageX, messageY);
  }

  public void mouseDragged(MouseEvent e) {
   //para el evento drag and drop se invoca este método.
   // Guarda las coordenadas del ratón y muestra el mensaje.
   messageX = e.getX( );
   messageY = e.getY( );
   repaint( );
  }

  public void mouseMoved(MouseEvent e) {}
 // no se necesita así que no se implementa

  public static void main(String[] args) {
   JFrame f = new JFrame("HelloJava2");
   // Hace que la aplicación termine cuando se cierre la ventana.
   // WindowAdapter se utiliza para crear listeners para los eventos de la ventana, y se redefine en línea el método windowClosing
   f.addWindowListener(new WindowAdapter( ) {
    public void windowClosing(WindowEvent we) { System.exit(0); }
   });
   f.setSize(300, 300);
   f.getContentPane( ).add(new HelloJava2("Hola, Java!"));
   f.setVisible(true);
  }
}

//fichero: HelloJava3.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class HelloJava3 extends JComponent
implements MouseMotionListener, ActionListener {

  // Coordenadas del mensaje
  int messageX = 125, messageY = 95;
  String theMessage;

  //nuevo componente, un botón
  JButton theButton;

  // Índice del Color actual seleccionado del array
  int colorIndex;
  // array de colores, es static, porque se trata de una variable de clase.
  static Color[] someColors = { Color.black, Color.red,
  Color.green, Color.blue, Color.magenta };

// Constructor, crea el boton, lo añade al panel y establece quien maneja las acciones del botón, this.
  public HelloJava3(String message) {
   theMessage = message;
   theButton = new JButton("Cambiar Color");
   setLayout(new FlowLayout( ));
   add(theButton);
   theButton.addActionListener(this);
   addMouseMotionListener(this);
}

  public void paintComponent(Graphics g) {
   g.drawString(theMessage, messageX, messageY);
  }

  public void mouseDragged(MouseEvent e) {
   // Guarda las coordenadas del ratón y muestra el mensaje.
   messageX = e.getX( );
   messageY = e.getY( );
   repaint( );
  }

  public void mouseMoved(MouseEvent e) {}

  // este método es del interfaz ActionListener y se llamará cada vez que se ejecute un evento del tipo ActionEvent
  public void actionPerformed(ActionEvent e) {
   // comprobamos que se ha pulsado el botón
   if (e.getSource( ) == theButton)
    changeColor( );
  }

  // este método cambiará el índice del color y asignará el nuevo color como color para la fuente del componente
  // Esta sincronizado con el método currentColor porque nunca deben realizarse a la vez, para evitar errores con la variable colorIndex
  synchronized private void changeColor( ) {
   // Se pasa al siguiente color (se cambia el índice).
   if (++colorIndex == someColors.length)
    colorIndex = 0;
   setForeground(currentColor( )); // Se usa el nuevo color.
   repaint( ); // Se dibuja de nuevo para que podamos ver el cambio de color.
  }

  synchronized private Color currentColor( ) {
   return someColors[colorIndex];
  }

  public static void main(String[] args) {
   JFrame f = new JFrame("HelloJava3");
   // Hace que la aplicación termine cuando se cierre la ventana.
   f.addWindowListener(new WindowAdapter( ) {
   public void windowClosing(WindowEvent we) { System.exit(0); }
    });
   f.setSize(300, 300);
   f.getContentPane( ).add(new HelloJava3("Hola, Java!"));
   f.setVisible(true);
  }
}

//fichero: HelloJava4.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

// esta clase es una hebra, es la segunda forma de crear hebras, con Runnable
public class HelloJava4 extends JComponent
  implements MouseMotionListener, ActionListener, Runnable {

  // Coordenadas del mensaje
  int messageX = 125, messageY = 95;
  String theMessage;

  JButton theButton;

  int colorIndex; // Índice del Color actual seleccionado del array.
  static Color[] someColors = { Color.black, Color.red,
  Color.green, Color.blue, Color.magenta };

  // variable booleana para decir si parpadea o no.
  boolean blinkState;

  // el constructor crea además de todo lo anterior una hebra y la inicializa.
  // esta hebra se va a encargar de mostrar y ocultar el mensaje con una ejecución
  // separada a la ejecución secuencial de nuestro pequeño programa.
  public HelloJava4(String message) {
   theMessage = message;
   theButton = new JButton("Cambiar Color");
   setLayout(new FlowLayout( ));
   add(theButton);
   theButton.addActionListener(this);
   addMouseMotionListener(this);
   Thread t = new Thread(this);
   t.start( );
  }

 // según el valor de la variable blinkState, cambia el color a uno nuevo o al mismo color del fondo.
  public void paintComponent(Graphics g) {
   g.setColor(blinkState ? getBackground() : currentColor( ));
   g.drawString(theMessage, messageX, messageY);
  }

  public void mouseDragged(MouseEvent e) {
   messageX = e.getX( );
   messageY = e.getY( );
   repaint( );
  }

  public void mouseMoved(MouseEvent e) {}

  public void actionPerformed(ActionEvent e) {
   // comprobamos que se ha pulsado el botón
   if (e.getSource( ) == theButton)
     changeColor( );
  }

  synchronized private void changeColor( ) {
   // Se pasa al siguiente color (se cambia el índice).
   if (++colorIndex == someColors.length)
    colorIndex = 0;
   setForeground(currentColor( )); // Se usa el nuevo color.
   repaint( ); // Se dibuja de nuevo para que podamos ver el cambio de color.
  }

  synchronized private Color currentColor( ) {
   return someColors[colorIndex];
  }

 // este método es el que ejecuta la hebra independiente e irá repintando cada medio segundo el mensaje
  public void run( ) {
   try {
    while(true) {
     blinkState = !blinkState; // Toggle blinkState.
     repaint( ); // Show the change.
     Thread.sleep(500);
    }
   }
   catch (InterruptedException ie) {}
  }

  public static void main(String[] args) {
   JFrame f = new JFrame("HelloJava4");
   // Hace que la aplicación termine cuando se cierre la ventana.
   f.addWindowListener(new WindowAdapter( ) {
    public void windowClosing(WindowEvent we) { System.exit(0); }
   });
   f.setSize(300, 300);
   f.getContentPane( ).add(new HelloJava4("Hola, Java!"));
   f.setVisible(true);
  }
}

Copia las clases anteriores a ficheros y compila y ejecuta para observar cual es su salida.
(cuidado con los comentarios que ocupan más de una línea, ya que al copiarlos pueden quedar líneas de comentario sin su correspondiente '//' y producir errores de compilación)

Ir a Inicio

 

 

Uno con Errores

A continuación se muestra un ejemplo sencillo que contiene varios errores fáciles de identificar para que el alumno se vaya familiarizando con ellos (aunque seguro que cuando comience a programar acabará odiándolos, cosa normal).

//fichero: EjemploErrores.java
class EjemploErrores {

  public static void main(String arg[]) {

    System.out.println("Comienza main()...);
    Circulo c = new Circulo(2.0, 2.0, 4.0);
    System.out.prntln("Radio = " + c.r + " unidades.");
    System.out.println("Centro = (" + c.x + "," + c.y + ") unidades.");
    Circulo c1 = new Circulo(1.0, 1.0, 2.0);
    Circulo c2 = Circulo(0.0, 0.0, 3.0);
    c = c1.elMayor(c2);
    System.out.println("El mayor radio es " + c.r + ".");
    c = new Circulo(); // c.r = 0.0;
    c = Circulo.elMayor(c1, c2);
    System.out.println("El mayor radio es " + c.r + ".")

    out.println("Termina main()...");

  } // fin de main()

} // fin de class EjemploErrores


class Circulo {
   static int numCirculos = 0;
   public static final double PI=3.14159265358979323846;
   public double x, y, r;

   // constructor
   public Circulo(double x, double y, double r) {
     this.x=x; this.y=y; that.r=r;
     numCirculos++;
   }

   public Circulo(double r) { this(0.0, 0.0, r); }
   public Circulo(Circulo c) { this(c.x, c.y, c.r); }
   public Circulo() { this(0.0, 0.0, 1.0); }
   public double perimetro() { return 2.0 * PI * r; }
   public double area() { retur PI * r * r; }

   // método de objeto para comparar círculos
   public Circulo elMayor(Circulo c) {
     if (this.r>=c.r return this; else return c;
   }

   // método de clase para comparar círculos
   public static Circulo elkMayor(Circulo c, Circulo d) {
     if (c.r>=d. r) return c; else return d;
   }

} // fin de la clase Circulo

Compila y ejecuta el ejemplo anterior (copiándolo a un fichero). Corrige los errores que tenga para ver cómo funciona.

Ir a Inicio

 

 

Una Ronda de Ejercicios

Este capítulo se propondrán una serie de ejercicios de dificultad creciente basados en todo lo visto anteriormente, a fin de que el alumno afiance los conceptos del curso.

Sobre los ejemplos

Ejercicio 1:
Modifica las clases anteriores para que muestren la hora actual en lugar del mensaje "Hola, Java!". Ver paquete java.util, clase Calendar y subclase GregorianCalendar.

Tipos Básicos

Ejercicio 2:
Crea un programa java que simplemente muestre por pantalla cual es la inicialización por defecto que Java da a cada tipo básico: boolean, char, byte, short, int, long, float y double. Utiliza una clase que sólo tenga la función main y muestra por pantalla los valores con System.err.println("Tipo " + variable);

Ejercicio 3:
Crea un método que cree e inicialice un array unidimensional del tipo double. El tamaño del array será determinado por un parámetro de este método y los valores que se utilizan para la inicialización estarán dentro de un rango determinado por dos parámetros del método (inicializar de manera secuencial incrementando desde el menor al mayor del rango). Crea otro método que muestre por pantalla el array ya inicializado. Comprueba que estos dos métodos funcionan dentro de un programa adicional.

Ejercicio 4:
Modifica el ejercicio anterior para utilizar un array bidimensional.

Ejercicio 5:
Escribe una función que realice todas las posibles comparaciones lógicas entre dos cadenas, incluyendo, equals().

Ejercicio 6:
Escribe un programa que genere 25 números aleatorios y comprueba si están por encima o por debajo de un valor intermedio. Almacena los mayores en un array y los menores en otro array. Muestra los resultados finales por pantalla.

Clases y Objetos

Ejercicio 7:
Crea una clase que se llame MiClase con dos variables miembro.
7.a) Crea un constructor sin parámetros que inicialice las variables miembro de MiClase.
7.b) Sobrecarga el constructor anterior para que reciba los valores con los que inicializar las variables.
7.c) Modifica el código del constructor sin parámetros para que utilice el segundo constructor para inicializar las variables.
7.d) Crea un método toString() dentro de la clase MiClase que devuelva un String con el valor de las dos variables.
7.f) Compila y ejecuta MiClase. Crea dentro del método main una instancia de MiClase y llama al método toString().
7.g) Crea un método finalize() que muestre un mensaje por pantalla con System.out.println("finalize()"). Incluye en el programa las llamadas a los métodos System.gc(), System.runFinalizersOnExit(true). Ejecuta para ver qué pasa.
7.h) Añade una variable publica Static dentro de la clase. Muestra por pantalla el valor de la variable static en los constructores de la clase. Declara dos objetos de la clase MiClase e inicializa la variable Static antes, en medio y después de instanciarlos (con new).
7.i) Añade una variable del tipo java.util.Collection a la clase MiClase. Inicializa la variable Collection en los constructores de las clases y añade varios elementos a la misma. Busca en la documentación qué métodos se pueden utilizar con esta clase y utilízalos dentro de un nuevo método publico denominado interfaceC. Muestra la salida de cada método por pantalla.
7.j) Crea otra variable de tipo java.util.Vector. Inicializa la variable Vector en los constructores de la clase y añade unos cuantos elementos al mismo. Busca en la documentación qué métodos se pueden utilizar con esta clase y utilízalos dentro de un método denominado interfaceV. Muestra la salida de cada método por pantalla.

Ejercicio 8:
Crea dos clases A y B con constructores sin parámetros que muestren un mensaje al ser ejecutados. Crea una clase C sin constructor que herede de A y que tenga una variable de tipo B.
8.a) Crea una instancia del tipo C y observa qué pasa al ejecutar el programa. (muestra mensajes dentro de los constructores y dónde quieras para comprobar lo que ocurre).
8.b) Cambia los constructores de la clase A y B para que utilicen un parámetro. Compila, ejecuta y observa qué ocurre.

Ejercicio 9:
Crea una clase base con dos métodos. Dentro del primer método llama al segundo método. Crea una clase hija que redefina el segundo método. En el programa principal, crea una instancia de la clase hija. Haciendo un casting de la instancia a la clase base, llama al primer método y explica cual es la salida.

Ejercicio 10:
Crea una clase que contenga una clase miembro (incluida en la anterior) que a su vez contenga una clase miembro (incluida en ésta). Compila y observa los ficheros que produce el compilador.

Ejercicio 11:
Crea una clase que se llame Perro y que tenga un método sobrecargado llamado ladrido. Sobrecárgalo de tal manera que el tipo de parámetro determine el tipo de ladrido. Muestra el tipo de ladrido con un mensaje. Dentro del programa principal llama al método ladrido con diferentes parámetros.

Interfaces y Packages

Ejercicio 12:
Crea un interfaz perteneciente a un paquete e impleméntalo en otra clase de otro paquete.

Ejercicio 13:
Crea un programa que demuestre que las variables definidas en un interfaz son static y final y que los métodos son por defecto públicos.

Ejercicio 14:
Crea tres interfaces diferentes y demuestra que Java permite la implementación (implements) de varios interfaces, que java permite la herencia multiple de interfaces y que no permite la herencia múltiple entre clases.

Sobre otro Ejemplo (de todo un poco)

Dado el siguiente ejemplo:

//fichero: Forma.java

class Forma {
   void dibujar() {}
   void borrar() {}
}

class Circulo extends Forma {
   void dibujar() {
     System.out.println("Circulo.dibujar()");
   }
   void borrar() {
     System.out.println("Circulo.borrar()");
   }
}

class Cuadrado extends Forma {
   void dibujar() {
     System.out.println("Cuadrado.dibujar()");
   }
   void borrar() {
     System.out.println("Cuadrado.borrar()");
   }
}

class Triangulo extends Forma {
   void dibujar() {
     System.out.println("Triangulo.dibujar()");
   }
   void borrar() {
     System.out.println("Triangulo.borrar()");
   }
}

public class Formas {
   public static Forma formaAleatoria() {
     switch((int)(Math.random() * 3)) {
       default:
       case 0: return new Circulo();
       case 1: return new Cuadrado();
       case 2: return new Triangulo();
   }
  }
  public static void main(String[] args) {
     Forma[] s = new Forma[9];
     // Rellenar el array con varias formas:
     for(int i = 0; i < s.length; i++)
       s[i] = formaAleatoria();
     // llamar a los método para ver el polimorfismo:
     for(int i = 0; i < s.length; i++)
       s[i].dibujar();
   }
}

Compila y ejecuta el ejemplo para ver qué ocurre

Ejercicio 15:
Modifica el ejemplo anterior creando una nueva clase denominada pentágono que herede de Forma y que sobrecargue los métodos dibujar y borrar. Compila y ejecuta para ver qué ocurre.

Ejercicio 16:
Crea un interfaz llamado manejo que contenga los métodos dibujar y borrar y modifica el ejemplo anterior para que la clase Forma implemente el interfaz manejo. Compila y ejecuta. ¿qué ocurre?

Ejercicio 17:
Separa cada una de las clases en un fichero diferente y hazlas públicas. Elimina de la clase Triangulo el método borrar. Compila y ejecuta para ver qué ocurre.

Ejercicio 18:
Convierte los métodos borrar y dibujar a abstractos. Compila y ejecuta para ver qué ocurre.

Ir a Inicio

 

 

Hebras (Threads)

Una de las principales ventajas del entorno de ejecución Java es que permite manejar programas que contienen múltiples procesos llamados Threads. La idea es crear aplicaciones que puedan realizar a la vez diferentes tareas o, al menos, hacer creer a los usuarios que se están realizado varias cosas simultáneamente. Por ejemplo, una aplicación multithreaded que gestiona un almacén será capaz de recibir datos por la red, realizar cálculos y a la vez, aceptar la entrada del usuario. Aunque en realidad sólo se ejecutará un thread a la vez, el sistema operativo que ejecuta el programa va ejecutando durante cierto tiempo un thread y luego saltará al siguiente thread, pero al usuario le dará la impresión de que se están ejecutando todos a la vez.Para entender cómo se codifican los threads, vamos a ver un ejemplo que contiene un solo proceso. En este ejemplo, el código se ejecuta de forma lineal hasta que se llega a la última línea, entonces el proceso termina.

public class Saludo {
  // MÉTODOS
  public void saluda (String quien) {
   System.out.println (quien + " ha saludado");
  }
    // MAIN
  public static void main (String args[]) {
 
   Saludo presentador = new Saludo();
   presentador.saluda ("persona1");
   presentador.saluda ("persona2");
   presentador.saluda ("persona3");
   System.out.println ("Todo el mundo ha saludado");
 }
}

La ejecución comienza y se crea una instancia de la clase Saludo. Luego se llama tres veces al método saluda(). Cada llamada se ejecuta y luego se devuelve el control al programa principal. La última sentencia del main() escribe "Todo el mundo ha saludado" y entonces el programa termina. Al introducir los threads en este programa, se romperá la linealidad de la ejecución. El programa se dividirá en fragmentos y cada uno de los cuales se encargará de escribir el mensaje en pantalla.

public class Saludo implements Runnable {
  // METODOS
  public void run () {
   System.out.println (Thread.currentThread().getName() + " ha saludado");
  }
  // MAIN
  public static void main (String args[]) throws InterruptedException {
   int i = 0;
   Saludo presentador = new Saludo();
   // Se crea el primer thread
   Thread unThread = new Thread (presentador, "persona1");
   // Se crea el segundo thread
   Thread otroThread = new Thread (presentador, "persona2");
   unThread.start(); // se inicia el primer thread
   otroThread.start(); // se inicia el segundo thread
   // Cuerpo del programa principal
   while (unThread.isAlive() || otroThread.isAlive()) i++;
   // Se ejecuta después de que los threads hayan terminado
   System.out.println ("El valor de i es " + i );
   System.out.println ("Todo el mundo ha saludado");
  }
}

Ahora el programa inicia dos threads que se ejecutan concurrentemente a la vez que el programa principal. Después de crear los threads, se llama al método start() de cada uno, lo que le dice al intérprete de Java que comience a procesar ese thread. El método main() es el responsable de inicializar cada thread y de determinar cuando termina. Esto es necesario porque el programa necesita saber cuando es seguro ejecutar la línea de código:

System.out.println ("El valor de i es " + i );

De otro modo, el programa podría terminar antes de que los threads hayan concluido, quedando bloqueado. Para controlar esto, se ha puesto un bucle que incrementa el valor de una variable mientras los threads se están ejecutando.

while (unThread.isAlive() || otroThread.isAlive()) i++;

Si se compila y ejecuta este programa, se podrá comprobar que el valor de la variable i es diferente en cada ejecución del programa. Esta variable almacena el número de veces que se ejecuta el bucle while mientras se ejecutan los threads. El hecho de que este valor cambie, ilustra como Java ejecuta los programas que se dividen en distintos procesos.
Ir a Inicio

 

Cuándo usar hebras

Lo más importante de la programación con threads es conocer cuándo se necesita usarlas. Los threads pueden ayudar a que un programa se ejecute más rápido pero también pueden entorpecer la ejecución del mismo. Solamente deben utilizarse cuando se necesite que dos o más procesos se ejecuten a la vez. Por ejemplo, en un entorno de ventanas, se pueden abrir varias ventanas a la vez para dar la impresión de que varias operaciones están ocurriendo al mismo tiempo.

Reiteramos la idea de que cuando una aplicación se está ejecutando, los distintos procesos no se ejecutan realmente al mismo tiempo. Es el sistema operativo el que se ocupa de dar al usuario la impresión de que todo está sucediendo simultáneamente. La máquina virtual Java maneja la gestión del procesador determinando qué ejecuciones deben ocurrir y en qué orden.
Ir a Inicio

 

Cómo se Crea una Hebra

Antes de crear un thread, se debe inicializar una clase que sea la encargada de manejar el thread. Esto puede hacerse de dos formas: derivando una clase (haciendo una subclase de la clase Thread) o implementando un interface.

Heredando de Thread

La forma más obvia de crear un thread es haciendo una subclase de la clase Thread que proporciona la librería Java. Esta aproximación permitirá sobreescribir los métodos de dicha clase para realizar las funciones que sean necesarias en el programa. Aquí está la sintaxis para crear una clase derivando de la clase Thread:

[visibilidad] [modificador] class NombreClase extends Thread
   // definición de variables y métodos
   ...
}

Veámoslo utilizando este ejemplo.

public class Saludo extends Thread {
  // MÉTODOS
  public void run () { System.out.println ("ha saludado");}

  // MAIN
  public static void main (String args[]) {

   int i = 0;
   Saludo presentador = new Saludo();

   // Se crea el primer thread
   Thread unThread = new Thread (presentador);

   // Se crea el segundo thread
   Thread otroThread = new Thread (presentador);

   unThread.start(); // se inicia el primer thread
   otroThread.start(); // se inicia el segundo thread

   // Cuerpo del programa principal
   while (unThread.isAlive() || otroThread.isAlive())
    i++;

   // Se ejecuta después de que los threads hayan terminado
   System.out.println ("Todo el mundo ha saludado");

  }
}

Ahora la clase Saludo deriva de la clase Thread y sobreescribe el método run() definido en esta clase (al heredar Saludo de Thread, contendrá todos los métodos de Thread como start() y run()). En el ejemplo se mantienen las versiones originales de los métodos de la clase Thread excepto en el caso del método run(), que ha sido redefinido. Dicho método es el que le dice al thread qué operaciones tiene que realizar una vez que éste ha sido iniciado, por eso normalmente este método debe ser sobrecargado.

Implementando Runnable

Cuando se diseña la jerarquía de clases para la aplicación, hay veces que es imposible derivar de la clase Thread (porque las clases ya deriven de otras, por ejemplo). En estos casos, se puede implementar el interface Runnable para solucionar el problema. La sintaxis es:

[visibilidad] [modificador] class NombreClase [extends SuperClase] implements Runnable
   // definición de variables y métodos
   ...
}

La ventaja de esta forma es que se puede crear una nueva clase que deriva de otra clase y además utilizar los métodos definidos por el interface Runnable.

public class Saludo implements Runnable {
  // MÉTODOS
  public void run () {
   System.out.println (Thread.currentThread().getName() + " ha saludado");
  }

  // MAIN
  public static void main (String args[]) throws InterruptedException {
   int i = 0;
   Saludo presentador = new Saludo();
   // Se crea el primer thread
   Thread unThread = new Thread (presentador, "persona1");
   // Se crea el segundo thread
   Thread otroThread = new Thread (presentador, "persona2");
   unThread.start(); // se inicia el primer thread
   otroThread.start(); // se inicia el segundo thread
   // Cuerpo del programa principal
   while (unThread.isAlive() || otroThread.isAlive()) i++;
   // Se ejecuta después de que los threads hayan terminado
   System.out.println ("El valor de i es " + i );
   System.out.println ("Todo el mundo ha saludado");
  }
}

Ir a Inicio

 

 

Inicialización de una Hebra

Antes de que pueda utilizarse un thread, debe inicializarse creando una instancia de la clase Thread. El mejor modo de hacer esto es utilizando el constructor de la clase Thread. La forma más simple de este constructor es:

Thread identificador = new Thread();

Otras variaciones de este constructor son:

Thread identificador = new Thread(ReferenciaAObjeto);
Thread identificador = new Thread(Nombre);
Thread identificador = new Thread(ReferenciaAObjeto, Nombre);

El parámetro ReferenciaAObjeto indica la referencia al objeto de la clase que implementa el método run() y que es el que debe ejecutarse cuando el thread comience su ejecución. El parámetro Nombre se utiliza para poner un nombre a cada uno de los threads por si se quiere distinguirlos. Por ejemplo:

Thread unThread = new Thread (presentador, "persona1");
Thread unThread = new Thread (presentador);

Aunque sólo puede iniciarse un Thread cada vez, el orden en que se llaman los threads no tiene por qué determinar necesariamente el orden en el que terminan. Volvemos al ejemplo Saludo para ilustrar cómo es difícil de predecir el orden de ejecución de los threads.

public class Saludo implements Runnable {
  // MÉTODOS
  public void run () {
   System.out.println (Thread.currentThread().getName() +
    " ha saludado");
  }
 
  // MAIN
  public static void main (String args[]) throws InterruptedException {
 
   int i = 0;
   int j = 0;
 
   Saludo presentador = new Saludo();
 
   // Se crea el primer thread
   Thread unThread = new Thread (presentador, "persona1");
 
   // Se crea el segundo thread
   Thread otroThread = new Thread (presentador, "persona2");
 
   unThread.start(); // se inicia el primer thread
   otroThread.start(); // se inicia el segundo thread
 
   // Cuerpo del programa principal
   while (unThread.isAlive() || otroThread.isAlive()) {
  
    // Contador del primer thread
    if (unThread.isAlive())
     i++;
  
    // Contador del segundo thread
    if (otroThread.isAlive())
     j++;
   }
 
   // Se ejecuta después de que los threads hayan terminado
   System.out.println ("El valor de i es " + i );
   System.out.println ("El valor de j es " + j );
  
   System.out.println ("Todo el mundo ha saludado");
  
  }
}

Como los dos threads se crean utilizando el mismo objeto, se pasa un string para distinguir un thread del otro. En el método run(), el método getName() se utiliza para imprimir el nombre del thread que se está ejecutando. En este ejemplo se ha añadido un contador para cada uno de los threads. El planificador contenido en la máquina virtual Java es el responsable de determinar qué threads pueden ejecutarse y cuales deben esperar en la cola. Esta decisión puede tomarse de dos formas: por prioridad o FIFO (First in, First Out. Por orden de llegada a la cola).
Ir a Inicio

 

Prioridades

Cuando se procesa un thread, éste se mete automáticamente en una cola, en la que debe esperar hasta que le llegue el turno de ejecutarse. Este proceso de espera se llama bloqueo.
En la aproximación FIFO, el thread que está el primero de la cola esperará hasta que termine el que se está ejecutando y entonces empezará su ejecución.
En la planificación por prioridades, en cambio, si un thread tiene una prioridad mayor que otro le cambiará su lugar dentro de la cola. Este proceso continuará hasta que se encuentre un thread con una prioridad mayor o igual. De forma que los threads que estén al final de la cola no se ejecutarán hasta que los de alta prioridad hayan terminado. El caso más común es el caso del thread Garbage Collector, que tiene la prioridad más baja.

Para controlar la prioridad de un thread, la clase Thread proporciona las variables: MAX_PRIORITY, NORM_PRIORITY y MIN_PRIORITY.
Para establecer y obtener la prioridad de un thread se utilizan los métodos setPriority() y getPriority().
Ir a Inicio

 

Vida de una Hebra

Un thread tiene un nacimiento, una vida y una muerte. Durante estos estados, un thread puede seguir diferentes alternativas dependiendo del objetivo que se quiera conseguir. Las diferentes etapas de un thread las determina un conjunto de métodos predefinidos que pueden sobrescribirse para realizar las tareas pertinentes.
En las siguientes secciones describiremos cada una de estas etapas, así como los métodos y estados destacados.

Creación

Para crear un nuevo thread debemos llamar al constructor de la clase como se muestra en el ejemplo siguiente:

public void start () {
  if (miThread == null) {
   miThread = new Thread(this);
   miThread.start();
  }
}

Una vez que se crea la instancia tenemos un nuevo thread en estado CREADO. En este estado el thread es simplemente un objeto thread vacío. Aún no se ha reservado ningún recurso del sistema para él. Cuando un thread está en este estado, lo único que se puede hacer con él es iniciarlo. Si llamamos a cualquier otro método se producirá una excepción del tipo IllegalThreadStateExcepcion.

Comienzo

Veamos el método que se muestra en el siguiente ejemplo:

public void start () {
  if (miThread == null) {
     miThread = new Thread(this);
     miThread.start();
  }
}

El método start() crea los recursos necesarios para que el thread pueda ejecutarse, planifica cuando debe comenzar el thread y lanza el método run(). Cuando el método start() termina, el thread pasa al estado RUNNABLE. Si tenemos en cuenta que muchos ordenadores sólo tienen un procesador, es imposible ejecutar a la vez todos los threads que están en este estado. Por esta razón, la máquina virtual Java debe implementar un mecanismo de planificación de forma que todos los threads compartan el procesador. Por tanto un thread en estado RUNNABLE realmente se encuentra esperando su turno para ser ejecutado en la CPU.

Run [Método]

En este método se definen las acciones que deben realizarse durante la vida del thread. Por ejemplo:

public void run() {
  Thread current = Thread.currentThread();
  while (miThread == current) {
   try {
    Thread.sleep(50);
   }
   catch (InterruptedException e) {
  }
  System.out.println ("Sigo vivo");
  }
}

El método run() se ejecutará mientras la condición del bucle miThread == current sea cierta. Esta condición de salida se explicará con más detalle más adelante. Por ahora, nos quedaremos con la idea de que esta condición permitirá que el thread termine correctamente.

Runnable (y Not Runnable) [Estados]

Un thread pasa al estado NOT RUNNABLE cuando se producen uno de estos eventos:

En el ejemplo, el thread miThread pasa al estado NOT RUNNABLE cuando el método run() llama al método sleep().

public void run() {
   Thread current = Thread.currentThread();
   while (miThread == current) {
    try {
     Thread.sleep(50);
    }
     catch (InterruptedException e) {
    }
   System.out.println ("Sigo vivo");
  }
}

Durante los 50 milisegundos que el thread está dormido (sleep()), no se ejecutará aunque el procesador esté disponible. Cuando haya pasado ese tiempo, el thread pasará al estado RUNNABLE otra vez y se ejecutará cuando el procesador esté disponible.
Los threads pasan de nuevo al estado RUNNABLE cuando se produzca uno de los siguientes eventos:

Sleep [Método]

Este método libera al thread del control del procesador durante una cantidad de tiempo especificada. La sintaxis de este método es:

Thread.sleep(milisegundos);

En el método run() del ejemplo anterior, se llama al método sleep() para permitir que se ejecuten otros threads mientras miThread está durmiendo.

Cómo se para un Thread

Un thread muere de forma natural cuando su método run() termina. En el ejemplo anterior la condición para el que bucle termine y con él, el método run() es miThread == current. Esta condición indica que el bucle terminará cuando el thread que está actualmente en ejecución no sea igual a miThread.

public void run() {
  Thread current = Thread.currentThread();
  while (miThread == current) {
   try {
    Thread.sleep(50);
   }
    catch (InterruptedException e) {
   }
   System.out.println ("Sigo vivo");
  }
 }

Cuando se quiera parar el thread, alguien debe llamar a al método sleep, (wait(), stop() también se pueden utilizar aunque en algunas versiones de java están en desuso) que parará la hebra de forma temporal. De esta forma se le está diciendo al bucle del método run() que pare durante un tiempo. En estas pausas se puede modificar alguna variable de control del bucle que es la que debe provocar que el metodo run() termine de forma definitiva y por lo tanto la hebra desaparezca.

IsAlive [Método]

Este método devolverá true si el thread ha sido iniciado y no está parado. Si el método devuelve false, entonces sabremos que el thread está en estado CREADO o en estado MUERTO. Si devuelve true el thread estará en estado RUNNABLE o NOT RUNNABLE.

Yield [Método]

Este método provoca que el thread que se está ejecutando en un momento dado se mueva al final de la cola para permitir que el siguiente thread sea procesado.

public void run() {
  while (miThread != null) {
   System.out.println ("Sigo vivo");
   yield();
  }
}

Ir a Inicio

 

 

Compartiendo Objetos

Cuando en una aplicación existen varios threads ejecutándose a la vez, surge la necesidad de limitar el acceso a ciertas partes del código donde el acceso simultáneo de varios procedimientos puede provocar un mal funcionamiento de la aplicación.

Sincronización

Java utiliza la idea de monitores para sincronizar el acceso a los datos. Un monitor es un lugar protegido en el que todos los recursos protegidos tienen los mismos bloqueos. Existe solo una llave para desbloquear todos los bloqueos dentro del monitor y los threads deben obtener esa llave para entrar en el monitor y acceder a los recursos protegidos. Si muchos threads quieren entrar en el monitor al mismo tiempo, dado que sólo un thread tiene la llave, los otros deben esperar fuera hasta que el thread que tiene la llave finalice y devuelva la llave a la maquina virtual Java. Una vez que el thread tiene la llave del monitor, puede acceder a cualquiera de los recursos que controla el monitor tantas veces como quiera, mientras posea la llave. Sin embargo, si el thread quiere acceder a los recursos controlados por otro monitor, debe obtener la llave de ese otro monitor. En un instante determinado, un thread puede poseer varias llaves y diferentes threads pueden tener diferentes llaves al mismo tiempo. Cuando varios threads están esperando recíprocamente las llaves de otros puede producirse el efecto de interbloqueo o deadlock.

En Java, los recursos protegidos por los monitores son fragmentos del programa en forma de métodos o bloques de sentencias encerrados entre llaves. La palabra clave synchorized se utiliza para indicar que el siguiente método o bloque de sentencias esta sincronizado por un monitor. Cuando se utiliza el modificador synchronized en un método, el objeto donde está incluido el método sólo puede ser accedido por un thread a la vez. Cuando un thread entra en una porción de código sincronizada, es bloqueado hasta que el thread que está por delante de él finalice la ejecución de ese código.

Wait y Notify [Métodos]

Supongamos que estamos ejecutando un thread dentro de un método sincronizado y que en la mitad del método se necesita recoger cierta información que proporciona otro thread. Por ejemplo, el primer thread puede abrir un fichero, el siguiente thread (una vez abierto) puede entrar en el método y escribir algo en el fichero y para terminar, el primer thread realizará las operaciones de limpieza necesarias y cerrará el fichero. Esto sería lo ideal, pero si tenemos en cuenta que estamos en un método sincronizado, sólo se permitirá que un thread lo ejecute. Para solucionar esta situación, se utiliza el método wait(), que provoca que el siguiente thread entre dentro del método sincronizado. El thread que ha abandonado el método, puede volver a él cuando se llama al método notify() desde dentro del método que nos ocupa, volviendo a ejecutar el método desde el punto donde lo dejó.

public synchronized void run() {
  int d = 0;
 
  while (d < 100) {
   System.out.println (Thread.currentThread().getName() + " " + d);
   d++;
   if (d == 50) {
    try {
    if (Thread.currentThread.getName().equals("Thread1")) {
      this.wait();
     }
    }
    catch (InterrumptedException e) {
     System.out.println ("ERROR");
    }
   }
  }
 
  if (Thread.currentThread().getName().equals("Thread2"))
   this.notify();
}

En este ejemplo, el thread con nombre "Thread1" contará hasta 50. Entonces abandonará el método y se lo dejará al segundo thread. El thread "Thread2" contará hasta 99 y notificará al primero para que vuelva a ejecutar el método desde el punto donde lo dejó. Ir a Inicio

 

Otra Ronda de Ejercicios

A continuación se proponen una serie de ejercicios relacionados con el tema de las hebras, a fin de que los alumnos comprendan totalmente este tema.

Ejercicio 1:
Crea una clase que herede de la clase Thread y redefine el método run() para que imprima un mensaje y llame al método sleep(). Repite esta operación 3 veces y después termina el método run(). Incluye un mensaje en el constructor y redefine el método finalize() para que imprima otro mensaje cuando la hebra deje de existir. Ahora crea otra clase que también sea una hebra y que en su método run() llame al System.gc() y a System.runFinalization() imprimiendo un mensaje cuando se invoque a esos métodos. Ejecutar varias veces para ver qué pasa.

Ejercicio 2:
Cambia el ejercicio anterior para que en vez de heredar de las clases, implementen el interfaz runable.

Ejercicio 3:
Crea tres hebras denominadas, Cliente, Servidor y Coordinador. El coordinador coordinará la actuación del Servidor y el Cliente. El Servidor irá creando tareas que serán almacenadas en un Vector de tareas del Coordinador y el Cliente irá procesando las tareas del vector de trabajo. La condición de parada de las hebras Coordinador y Servidor será por tiempo y el Cliente terminará cuando no le quede trabajo restante por procesar.

Ir a Inicio

 



Última actualización: 27/02/2006

Antonio M. Mora García
Dpto. de Arquitectura y Tecnología de los Computadores (Universidad de Granada)
Tlfno: 958240838     E-Mail:

Maria Isabel García Arenas
Dpto. de Informática, área de Arquitectura y Tecnología de los Computadores (Universidad de Jaen)
Tlfno: 953212897     E-Mail: