abstract tipo nombre (lista_de_parametros); |
Se puede hacer que ciertos métodos sean sobrescritos por las subclases utilizando el identificador de tipo abstracto. Es responsabilidad de la subclase implementarlo ya que no posee ninguna implementación de la superclase. La forma general:
Cualquier clase que contenga uno o más métodos abstractos se tiene que declarar como abstract, para declararla simplemente, se utiliza la palabra abstract al principio de la declaración de la clase. No se puede crear instancias de estos tipos de clases. Aunque sí pueden ser utilizadas para crear referencias a objetos. Es posible crear una referencia de una clase abstracta para que pueda ser utilizada como referencia a un objeto de una subclase. Gracias a este mecanismo se puede decidir en tiempo de ejecución cuál de las versiones de un método sobrescrito se debe ejecutar.
Para que un método haga referencia al objeto que lo invocó, Java define una palabra clave this, es siempre una referencia al objeto sobre el que ha sido llamado el método. Se utiliza siempre que se quiera hacer una referencia a un objeto del tipo de la clase actual.
Para definir métodos de una clase que puedan ser utilizados por sí mismo sin referirse a una instancia específica se precede en la declaración del método con la palabra clave static. El método puede ser referenciado sin haber creado ningún objeto de esa clase. El ejemplo más común es en la declaración de main() ya que es llamado sin que exista ningún objeto.
Super se puede utilizar para dos cosas: para llamar al constructor de la superclase y para acceder a un miembro de la superclase que ha sido ocultado por un miembro de la subclase.
Paquetes: son contenedores de clases que se utilizan para dividir
en compartimentos el espacio de nombres de clases, los paquetes son un
componente básico de los programas, para definirlos hay que utilizar
el comando package como primera sentencia del archivo fuente.
Cualquier clase que se defina dentro de este archivo pertenece a ese paquete,
estos definen un conjunto de espacios de nombres particionados en los que
se almacenan las clases.
Import paquete1. [paquete2].(nombre_clase | *) ; |
Todas las clases estándar están almacenadas en algún
paquete con nombre Java, con la sentencia import
importamos paquetes enteros en nuestros programas y así una clase
incluida en el paquete importado puede ser referenciada directamente. El
formato general se la sentencia import es:
[public ] nombre_interface{ tipo metodo1 (parámetros); tipo variable_final1=valor; tipo metodo2 (parámetros); tipo variable_final2=valor; ... tipo metodo (parámetros); tipo variable_finaln=valor; } |
Interfaces: En las clases que definimos como interfaces especificamos qué es lo que hace, pero no cómo lo hace. Tienen el mismo aspecto que una clase pero sin variables de instancias y con métodos declarados sin cuerpos. Definiendo una interfaz cualquier clase puede implementarla. Este mecanismo permite implementar totalmente el polimorfismo "una interfaz múltiples métodos". Las interfaces están es jerarquías distintas de las de las clases, por lo que es posible que varias clases sin relación implementen la misma interfaz. El formato general de una interfaz sería:
Observa que los métodos declarados no tienen cuerpo, son métodos abstractos ya que no se pueden implementar en la declaración de una interfaz, cada clase que incluye una interfaz debe implementar todos los métodos. El acceso a la interfaz es public o no se utiliza, en este último caso se utiliza el modificador de acceso por defecto y la interfaz estará solo disponible para los otros miembros de paquete en la que ha sido declarada. Si se declara public la interfaz puede ser utilizada por cualquier código.
Las variables se pueden declarar dentro de las declaraciones de la interfaz
y son implícitamente "final" y static
lo que indica que sólo se puede modificar la clase que las implementa
y además pueden ser inicializadas con un valor constante.
[public] class nombre_clase [extends superclase]
[implements interfaz1, [interfaz2]....] { //cuerpo } |
Una vez definida la interfaz una o varias clases pueden implementarla, para ello hay que incluir la sentencia implements en la definición de la clase y crear los métodos definidos por la interfaz. El formato general será:
El formato del método implementado debe coincidir exactamente con el formato del tipo especificado en la definición de la interfaz.
Las interfaces a igual que las clases pueden utilizar la herencia, incluyendo la palabra clave extends.
Para poder realizar ejemplos de todo los que llevamos hasta ahora , estudiaremos dos clases que nos serán muy útiles la clase String y las matrices.
Matrices: Las matrices, como casi todo el mundo sabe, son un grupo de variables del mismo tipo, a las que se hace referencia con el mismo nombre. Se pueden crear matrices de cualquier tipo y pueden tener una o más dimensiones. Además las matrices no son un tipo simple de datos por lo que, por definición, todo lo que no es un tipo simple de datos es un objeto en Java, y las matrices habrá que tratarlas como tal.
La forma general de la definición de una matriz es:
tipo nombre_matriz [] ;
unidimensional tipo nombre_matriz[][]...[] ; multidimensionales |
Nombre_matriz = new tipo [tamaño] |
Esta declaración establece una variable tipo matriz pero realmente no existe ninguna matriz hasta que no resolvemos memoria con el operador new.
Donde tipo específica el tipo de los datos que van a ser asignados, tamaño el número de elementos de la matriz. La memoria necesaria para cada matriz la reserva dinámicamente, una vez reservada se puede acceder a un elemento específico de la misma indicando su índice entre corchetes.
La definición de una matriz se puede hacer en una sola línea:
tipo nombre_matriz = new tipo [tamaño]; |
String: a igual que las tablas los string son otra estructura básica para cualquier lenguaje de programación y a igual que los array los String en Java no son un tipo básico por lo que se implementa como un objeto. Dispone de un conjunto completo de operaciones que facilita su utilización.
Hay algo importante en la clase String, cuando se crea un objeto de este tipo no puede ser modificado (no se pueden modificar los carácteres que forman la cadena). Java nos permite hacerlo aunque lo que ocurre es que creamos un nuevo objeto con las modificaciones ya realizadas, esta implementación la realiza Java por eficiencia del lenguaje. Existe una clase paralela a la String llamada StringBuffer que permite ser modificada.
Como toda clase que se precie la clase String y StringBuffer tiene varios
constructores:
String cadena = new String(); | Crea una instancia vacía |
Char car[] = {'a' ,'A','b','B'} String cadena = new String(car); | Crea una instancia de String, inicializandola con los caracteres de "car"(que es una array de tipo char) |
String cadena = new String(car,i,n); | Idem anterior pero indicando la posición de inicio(i) y numero de caracteres(n) que se utiliza. |
String c1 = new String(cadena); |
Si tenemos creado el string del ejemplo anterior, podemos hacer:
Con lo que las dos instancias creadas tendrán el mismo contenido.
String cadena = "aAbB"; |
Una forma muy común de crear un String es utilizar literales:
Ya que Java crea automáticamente un objeto String por cada literal de tipo cadena que haya en el programa.
Una vez creada nuestra instancia podemos realizar multitud de operaciones
que corresponde con los métodos definidos en esta clase, repasaremos
los más interesantes: donde c = "aAbB"
int length(); l = c.length(); | Longitud de una cadena, número de caracteres que tiene. |
int l=34; String c="tengo" + l + "años"; | Concatenación de caracteres incluso con tipos que no son String. |
char charAt(int pos) char ch; ch=c.charAt(1); | Extrae un carácter de una cadena. En nuestro ejemplo vale "a". |
Void getChars(int posin, int posfin,chardestino[],int desin); | Extrae más de un carácter a la vez, posin indica la posición inicial de la subcadena, posfin la posición siguiente donde termina la subcadena, en destino[] se obtendrá los resultados copiando la subcadena en la posición dentro de destino[] que indique desin. |
Char[] toCharArray(); | Convierte los caracteres de un objeto String en una cadena de caracteres. |
String toLowercase(); String toUpperCase(); | Convierte todos los caracteres de una cadena a mayúsculas o minúsculas respectivamente. |
String trim(); | Devuelve el string eliminado los espacios en blanco del principio y del final. |
ValueOf(); | Convierte tipos de datos a String. |
String substring(int posinicial); | Permite extraer un trozo de una cadena de un string, posinicial indica el inicio de la subcadena. |
String concat(); | Concatena cadenas. |
String replace(char original, char sustituto); | Sustituye en la cadena que llama al método todos los caracteres que coinciden con original por sustituto. |
boolean equals(object str);
boolean equalsIgnoreCase(object str); |
Copara cadenas para ver si son iguales.
Idem ignorando los caracteres mayúsculas y minúsculas. |
int indexOf(int ch); int lastIndexOf(int ch); | Devuelve la primera y última aparición de un carácter en una subcadena. |
StringBuffer insert (int indice, String str); | Inserta una cadena en otra. |
StringBuffer reverse(); | Invierte el orden de los caracteres de una cadena. |
Ejemplo: repasaremos en este ejemplo algunos de los temas que
hemos visto, en este ejemplo desarrollaremos una clase que nos sirva para
contar los distintos caracteres que le enviamos en un String. La clase
la instanciaremos desde otra clase principal que recibe la cadena a analizar
desde la línea de comandos:
CÓDIGO | COMENTARIOS |
---|---|
Class Contar{ char La_Matriz[] = new char[128]; void Caracteres(String La_Cadena){ int Indice; char car; for(Indice=0 ; Indice<La_Cadena.length() ; Indice++){ car = La_Cadena.charAt(Indice); ++La_Matriz[car]; } } void Resultado (){ int Indice, Resultado; char car; for (Indice=0;Indice < 128;Indice++){ car=(char)Indice; Resultado =(int)La_Matriz[Indice]; If (Resultado > 0){ System.out.println("del caracter " + car + " hay :" + Resultado + " Caracteres" ); } } } |
La clase contar define una matriz de caracteres de 128
ocurrencias, además se definen dos métodos:
Caracteres y resultado, los dos no devuelven ningún valor (void). El primero de ellos recibe como parámetro un string el cual vamos a analizar. Utilizamos dos métodos de la clase String vistos anteriormente: La_cadena.length() que nos devuelve la longitud de la cadena. Y La_cadena.charAt(Indice) que extrae un carácter del String de la posición indicada por Indice. Señalar que utilizamos car de tipo char como índice de la matriz. El método resultado simplemente devuelve el resultado de la operación de calcular las veces que se repite en una frase cada carácter. |
class Pcontar { public static void main(String arg[]) { String cadena = ""; int i; for(i=0; i<arg.length ;i++){ cadena = cadena + arg[i]; } Contar Frases = new Contar(); System.out.println(cadena); Frases.Caracteres(cadena); Frases.Resultado(); } } |
La clase Pcontar la utilizamos para instanciar la clase
Contar y recibir la frase a analizar para enviársela vía
parámetro al objeto creado. Como observamos es un programa que se
ejecuta en la línea de comando y recibe parámetros por medio
de un array de String arg[].
Concatenamos cada uno de las ocurrencias del parámetro de entrada(cada palabra de la frase esta en una ocurrencia de la matriz). Creamos el objeto Frases del tipo Contar y llamamos a los métodos de este: Caracteres, enviando como parámetro la frase a analizar y al método Resultado para que represente en pantalla el resultado el análisis. |
El paradigma de la orientación a objetos y la ejecución en subprocesos permite a usuario interactuar con varios objetos simultáneamente. De quí en adelante supondré que el lector conoce los conceptos básicos de programación concurrente: hilo de ejecución, sección crítica, semáforo, recurso etc. Estos conceptos están perfectamente explicados en cualquier buen libro sobre sistemas operativos.
Existen dos formas de crear una clase que admita ejecutarse en subprocesos múltiples:
Para crear subprocesos hijos extendiendo la clase Thread:
Class nombre_clase extends Thread { Public void run() { // cuerpo de ejecucion del subproceso. } } |
Para instanciar la clase y crear el subproceso concurrente con el hilo
principal haremos:
nombre_clase nombre_instancia = new nombre_clase(); |
Y llamaremos al método start() que invoca al método run(), iniciando el subproceso y volviendo al hilo principal.
Si lo hacemos con la interfaz Runnable:
Class nombre_clase implements Runnable { Public void run() { // cuerpo subproceso } }En el proceso principal haremos: Nombre_clase nombre_instancia = new nombre_clase; New Thread(nombre_instancia).Start(); |
Normalmente se utilizará este último método ya que por una norma no escrita se extiende una clase si ésta va a ser mejorada o sobrescrita y éste no es el caso de la clase Thread.
Otros métodos útiles para controlar el estado y ejecución
de un subproceso serán:
getName() | Devuelve una cadena con el nombre del subproceso. |
setName(String) | Da un nombre a un subproceso. |
currentThread() | Devuelve el subproceso en ejecución. |
isActive() | Comprueba si el subproceso esta activo. |
suspend() | Suspende la ejecución de un subproceso. |
resume() | Reanuda la ejecución de un subproceso. |
sleep(int) | Suspende el ssubproceso durante int milisegundos. |
yield() | El subproceso cede la ejecución a otro en espera. |
Cuando uno o varios subprocesos comparten un recurso, ya sea un dispositivo físico o simplemente variables y se pueden producir "condiciones de carrera" (esto es, errores de ejecución debido a una ejecución no secuencial) se debe implementar la sincronización entre los hilos para asegurar que solo uno está accediendo al recurso en un instante dado. Java proporciona un soporte único a nivel de lenguaje, utilizando el concepto de semáforo, este no es mas que un objeto que puede ser utilizado por un solo subproceso en un instante determinado. Esto se pude hacer por dos vías:
Restringiendo el acceso a un método, utilizando la palabra clave synchronized en la definición del método.Este segundo caso es útil ya que podemos utilizar clases de carácter general que en su definición sean sincronizadas o simplemente no dispongamos de su código fuente.
Class nombre_clase {
Syschronized void metodo () {la segunda vía sería con la sentencia syschronized, la llamada a los métodos se realiza dentro de un bloque de código sincronizado.
Sysnchronized (objeto){
// sentencias que deben ser sincronizadas
}
Java implementa métodos para facilitar la comunicación
eficiente entre subprocesos:
wait() | Indica al hilo en curso que abandone el semáforo (recurso sincronizado) y espere hasta que otro hilo entre en el mismo semáforo y llame a notify(). |
notify() | Activa el primer hilo que realiza la llamada a wait() |
notifyAll() | Despierta a todos los subprocesos que realizan llamadas a wait(). |
Ejemplo:
En el siguiente programa veremos como utilizamos la concurrencia e Java,
para ello creamos una clase que escribe en la salida estándar si
un número es divisible por otro. El programa principal recibe como
parámetro el limite de la serie y llama concurrentemente a la primera
clase de tres formas para obtener los múltiplos de 2, 3 y 5 de forma
concurrente.
CÓDIGO | COMENTARIOS |
---|---|
Class Cmultiplos implements Runnable { int limite, multiplo; Thread t; Cmultiplos (int h , int m){ limite = h; multiplo = m; t = new Thread(this, "hijo"); t.start(); } public void run() { for (int i = 1;i < limite + 1; i++) { int mod = i % multiplo; if (mod == 0){ System.out.println( i + " es multiplo de " + multiplo); } } } } |
Creamos la clase Cmultiplos para que pueda ser utilizada de forma concurrente utilizado en interfaz Runnable. |
class Multiplos { public static void main (String arg[]) { int parametro=Integer.valueOf(arg[0]).intValue(); new Cmultiplos(parametro,2); new Cmultiplos(parametro,3); new Cmultiplos(parametro,5); } } |
El programa principal llama a la clase en tres ocasiones enviándole parámetros distintos, con lo que se ejecutara esta clase de forma concurrente. |
try { // código que puede generar una excepción } catch (excepción 1 nombre){ // gestión excepcion1 } } catch (excepción2 nombre){ // gestión de excepcion2 } } finally { //código } |
Para manejar excepciones lo realizaremos con la ayuda de:
La entrada-salida más habitual será la realizada con los dispositivos por defecto: la consola como salida y el teclado como entrada (ya lo hemos utilizado en algunos ejemplos anteriores).
En la cima de la jerarquía de clases de E/S se encuentra las
clases InputStream y OutputStrean de las cuales se derivan
el resto de las clases, la entrada y salida estándar están
encapsuladas en la variables de objeto int y out de la clase System,
dichas variables de objeto son de los tipos InputStrean y OutputStream
respectivamente. Por lo que:
System.out
System.int System.err |
Flujo de salida estándar.
Flujo de entrada estándar. Flujo de error estándar. |
La salida estándar la conocemos de ejemplos anteriores por lo
que veremos la utilización de System.int con el método
principal que es Read(), el formato general será:
In Read() throws IOException |
Con este método, por cada llamada lee un único carácter y lo devuelve como un valor entero. Devuelve -1 cuando encuentra el final del flujo. Como podemos observar hay que controlar la excepción IOException.
Además de la E/S básico, el dispositivo más importante para comunicarse con el mundo exterior es el sistema de archivos. La clase File proporciona un método independiente para guardar información referente a un archivo, se utiliza para obtener modificar la información asociada a un archivo.
Los objetos File se pueden crear utilizando uno de los tres constructores
siguiente (los ejemplos utilizan un archivo en /usr/local/texto/articulo.txt):
File (string directorio);
Ejemplo: File dir = new File("/usr/local/texto"); |
|
Crea un objeto file basado en el directorio final. | |
File(String directorio, String archivo);
Ejemplo: File Arch=new File("texto","articulo.txt"); |
Crea una instancia de la clase file para un archivo especifico y no para el directorio completo. |
File(File obj, String archivo);
Ejemplo: File Arch= new File(dir,"articulo.txt"); |
Idem anterior, pero en lugar de definir el directorio como un string se define como un objeto. |
La clase file define muchos métodos, donde los más destacados
pueden ser:
MÉTODO | DESCRIPCIÓN |
---|---|
public String getName() | El nombre del archivo como String, no devuelve la estructura de directorios donde se encuentra. |
public String getPath() | El nombre del archivo en la clase File. |
public String getAbsolutePath() | El nombre del archivo, precedido de la ruta de acceso. |
public String getParent() | El nombre del directorio padre si existe si no null. |
public boolean exists() | Devuelve verdadero si el archivo existe el sistema. |
public boolean isFile() | Devuelve verdadero si la instancia de File es un archivo valido y normal. |
public boolean canWrite() | Devuelve verdadero si existe el archivo y este puede modificarse. |
public boolean canRead() | Devuelve verdadero si existe el fichero y este puede leerse. |
public int lastModified() | La última vez que se modifico el fichero. |
public boolean renameto(File dest) | Renombra el archivo actual con el nombre del archivo de la clase File. Devuelve true si ha tenido existo y false en caso contrario. |
public boolean mkdir() | Crea en la clase File el directorio relativo al directorio actual. Devuelve verdadero si el directorio se creo con existo y falso en caso contrario. |
public boolean mkdirs() | Idem caso anterior pero crea la estructura de directorio completa. |
public String list[] | Lista el contenido del directorio que se encuentra en la clase file. Devuelve una matriz con nombre de archivos menos "." y "..". |
FileInputStream : crea un InputStream que se
puede utilizar para leer el contenido de un archivo. Los constructores
más habituales son:
FileInputStream(String ruta) throws FileNotFoundExcption; |
FileInputStream(File obj) throws FileNotFoundException; |
Donde ruta es el nombre completo del directorio de un archivo y obj es un objeto de tipo File. Si el archivo no existe genera una excepción.
Métodos definidos por fileinputstream:
MÉTODO | DESCRIPCIÓN |
---|---|
int read() | Devuelve como entero el siguiente byte de la entrada. |
int read(byte bufer[]) | Intenta leer hasta bufer.length bytes situándolos en bufer y devuelve el número de bytes leídos con exito. |
int read (byte bufer[], int off, int len) | Intenta leer len bytes situándolos en bufer comenzando en bufer[off], devuelve el número de bytes que se leyeron con exito. |
int skip(long n) | Omite n bytes de la entrada y devuelve el numero de bytes que se han omitido. |
int available() | Devuelve el número de bytes disponibles para la lectura. |
void close() | Cierra el origen de la entrada. |
void mark(int n) | Coloca una marca en el punto actual del flujo de entrada que seguirá siendo valido hasta que se lean n bytes. |
void rest() | Devuelve el puntero de entrada a la marca establecida con el método anterior. |
boolean markSupported() | Devuelve true si se admite los métodos mark() o reset(). |
La clase FileOutputStream crea un OutputStream que
se utiliza para escribir en un archivo. Sus dos constructores más
habituales son:
FileOutputStream (String ruta) throws IOException;
FileOutputStream(File obj) throws IOException; |
Donde ruta es nuevamente el directorio completo de un archivo
y obj un objeto de tipo File, puede lanzar una excepción
IOException.
Los métodos más interesantes que define esta clase son:
MÉTODO | DESCRIPCIÓN |
---|---|
void write(int b) | Escribe un único byte en un fichero de salida. |
void write(byte buffer[]) | Escribe una matriz completa de bytes en un flujo de salida |
void write(byte bufer[], int n, int lon) | Escribe lon bytes de la matriz bufer, comenzando a partir de bufer[n]. |
void flush() | Inicializa la salida. |
void close() | Cierra el flujo de salida. |
Java permite los flujos con bufer, esto permite realizar las operaciones
de E/S con varios bytes a la vez incrementándose el rendimiento.
Se define las clases:
BufferdInputStream
BufferdOutputStream |
RandomAccessFile
RandomAccesFile(File obj, string modo)
RandomAccesFile(String archivo, String modo) |
Ofrece múltiples métodos para interacciones con un archivo, en esta clase se aúnan todas las funciones de las clases anteriores más la posibilidad de acceder a un archivo que se encuentre en cualquier lugar y además es posible determinar con exactitud la posición del archivo desde el cual se desea recuperar los datos. Se definen los siguientes constructores:
Donde obj especifica el nombre del archivo que se desea abrir a igual que archivo pero este como un String. En ambos casos modo determina el tipo de acceso al archivo:
w: lectura, escritura
Ejemplo :
En el siguiente ejemplo veremos como accedemos a un fichero secuencial,
leeremos carácter a carácter y como en un ejemplo anterior
calcularemos el numero de caracteres que hay distintos en el fichero:
CÓDIGO | COMENTARIOS |
---|---|
import java.io.*; class Fichero { public static void main(String args[]){ int i, j ; char c; char linea[] = new char [256]; String archivo= args[0]; FileInputStream fichero; try { fichero = new FileInputStream(archivo); }catch (FileNotFoundException e) { System.out.println("archivo no encontrado"); Return; } try { do { i = fichero.read(); if (i != -1) ++linea[i]; } while (i != -1); }catch(IOException e){ System.out.println("error e/s"); return; } try { fichero.close(); }catch(IOException e){ System.out.println("error e/s"); return; } for (j=0;j<256;j++) { if (linea[j] > 0) System.out.println("del caracter: " + (char)j + " hay :" + (int)linea[j]); } } } |
Importamos el paquete java.io, para poder realizar operaciones
con archivos. El nombre del archivo a analizar los recibimos por la línea
de parámetros args[].
Crearemos un objeto "fichero" de tipo FileInputStream, que posteriormente instanciaremos utilizando el constructor: FileInputStream(String archivo ) throws FileNotFoundExcption Ya que utilizamos la variable "archivo" que es de tipo String. Controlaremos la existencia del fichero con la excepción adecuada dando un mensaje de error por la salida estándar si no existe. A continuación leeremos el fichero carácter a carácter hasta que se encuentre el final del archivo, asimismo controlaremos que no se produce ningún tipo de error. Finalmente cerraremos el fichero y visualizaremos los resultados. La lectura de los archivos carácter a carácter tiene una sola ventaja a mi entender, que es la posibilidad de leer cualquier carácter Unicode y no quedarnos en los 256 caracteres del ASCII. |
public static final Double E=2.718281828459045idem para las funciones: cos, exp, floor, log, max, min, round, sin, sqrt, tan.
public static final Double PI=3.14592653589793
public static Double abs(Double a)
public static float abs(float a)
public static int abs(int a)
public static native Double acos(Double a)
public static native Double asin(Double a)
public static native Double atan(Double a)
public static final int ERA=0 public static final int YEAR=1 public static final int MONTH=2 public static final int SUNDAY=1 public static final int MONDAY=2 ....... public static final int JUNARY=0 public static final int FEBRUARY=1 ....... public static sysnchronized Calendar getinstance() public static sysnchronized Calendar getinstance(TimeZone, zone) ...... public int getFirstDaysinFirstWeek() public final Date getTime() protected void setTimeinMillis(long) public final void set(int year, int month, int date)
// constructores public Properties() public Properties(Properties defecto) // métodos public String getProperty(String key) public void list(PrintSttream out) public Enumeration propertyNames()
public String getHostName() public byte[] getAddress()
//constructores public ServerSocket(int puerto) public ServerSocket(in puerto, int ba) //métodos public Socket accept() public void close() public InetAdress getInetAdress()
Habrá observado que en estos capítulos y en estas paginas HTML no se ha encontrado nada en movimiento, es decir ningún Applet que distrajese nuestra vista. No soy muy partidario de ello pero como aperitivo a los capítulos siguientes que estarán dedicados a los paquetes gráficos y los applets incluiré aquí éste que me pareció muy interesante y está basado en el juego incluido en las librerías gráficas de Linux (y UNIX) y la idea viene del manual: Tutorial de Java. Manual en castellano en formato HTML obtenido de la dirección www.fie.us.es/info/internet/JAVA. Agustín Froufe.
He cambiado algunas cosas en el fuente para adaptarlo a mi forma de
ser (Los Ojos más redondos y con un poco de estrabismo). El fuente
va incluido con el artículo. (para activarlo hay que pasar el puntero
del ratón por la imagen).