El objetivo de esta unidad es crear algunos ejemplos de aplicaciones más o menos completas. Se presentan un par de aplicaciones cada una de las cuales consta de varias clases desarrolladas específicamente para la aplicación pero algunas de las cuales pueden servir para otras aplicaciones. Los ejemplos utilizan principalmente los conocimientos desarrollados en la primera parte de este curso y los temas de ficheros y corrientes de texto (material cubierto en la unidad 7 del curso Java Inicial) que serán de mucha utilidad en la siguiente unidad sobre el flujo de información en redes e internet. Enmedio de la unidad se hace una revisón y profundización del tema de las corrientes y se presentan las tuberías (pipes) para aplicarlas en el ejemplo de la última sección.
Para explorar el sistema de archivos de un ordenador existe una clase de java.awt llamada FileDialog. Aunque en las aplicaciones se recomienda usar esa clase, resulta interesante conocer cómo puede conseguirse su funcionalidad a partir de las clases básicas de java.io. El objetivo de esta sección es desarrollar un programa que permite explorar el sistema de archivos del ordenador, de manera semejante a como lo hace FileDialog, y además desarrollar un par de aplicaciones para visualizar ciertos archivos, es decir, se trata de desarrollar un visualizador general de archivos. En los ejercicios de la unidad se pide al alumno que aprenda a usar la clase FileDialog.
Primero desarrollamos una clase llamada exploraDirectorios que permite navegar por los directorios del sistema de archivos y mirar su contenido. Se trata de una aplicación directa de la clase java.io.File.
Primero desarrollamos una extensión de Frame que será de utilidad para varias aplicaciones: autoExitFrame.
Como puede comprobar el lector, se trata de una clase que agrega dos funcionalidades de uso frecuente en un marco:
Para que se cierre la ventana ha sido necesario implementar la interfaz WindowListener. Observe que el constructor de autoExitFile acepta la variable booleana solo que indica si el marco es un marco "principal" que al cerrarse debe hacer que termine la ejecución de la Máquina virtual de Java. Esto se logra llamando a System.exit(0) en caso de que solo==true. Por otro lado, el alumno debe observar cómo se realiza el trabajo de centrar el marco utilizando su tamaño y el de la pantalla que se obtiene con el método getScreenSize() de Toolkit. Ambas cosas son muy sencillas, pero como queremos que todos los marcos que vamos a utilizar tengan esta funcionalidad, las programamos de una vez por todas y a partir de ahora cuando queramos desarrollar un marco lo haremos como subclase de autoExitFrame.
El programa exploraDirectorios. consta de un marco que tiene un lista con todos los subdirectorios y archivos de un directorio cualquiera. El programa arranca en el directorio raíz, ("\" en Windows, "/" en Linux) . Arriba tiene un campo de texto donde aparece el nombre del directorio cuyo contenido se está mostrando en la lista y abajo aparece otro campo de texto con terminaciones de archivos que queremos que aparezcan. Si queremos que aparezcan todos basta limpiar el campo de texto de abajo. Si estamos usando Windows y queremos explorar otro disco debemos escribir en el campo de texto de arriba la dirección de ese disco, por ejemplo, "d:\" para explorar el disco D.
Este es el aspecto del programa explorarDirectorios cuando está explorando el directorio del curso Java Profundización:
Observe que cada aparecen primero los subdirectorios y luego los directorios y que todos tiene a su derecha la información del tamaño de archivo (cuando no es un directorio), la fecha y la hora de su última modificación y tres letras o guiones. Cuando hay letras en vez de guines éstas significan: a = archivo, r = lectura, w = escritura. El alumno debe ejecutar este programa con la llamada:
java unidad08.exploraDirectorios
y explorar su sistema de archivos con el programa para familiarizarse con su funcionamiento. Posteriormente deberá estudiar cuidadosamente el código y leer las explicaciones que siguen a continuación.
Este es el código del programa:
Primero conviene observar que la clase exploraDirectorios es extensión de autoExitFrame. por lo cual hereda toda su funcionalidad. Luego conviene observar que la construcción de la interfaz gráfica consta de dos campos de texto tf_path y tf_ext, destinados a contener la dirección del directorio que se está explorando y la lista de extensiones permitidas para los archivos, respectivamente. Estos campos de texto se colocan al norte y al sur del marco. Al centro se coloca una lista destinada a albegar la información de los archivos listados. La variable dir es la que se usa para contener la información de cual es el directorio que es está explorando. Esta variable se inicializa con el directorio raíz mediante:
dir=new File(File.separator);
El método leeDirectorio() es quien se encarga de rellenar toda la información en la lista. Observe que primero limpia la lista, luego pone la dirección absoluta del directorio dir en el campo de texto tf_path y procede a obtener la lista de todos los archivos aceptados, es decir los que terminan en alguna de las extensiones de la lista ext, mediante la llamada a la función dir.list(FilenameFilter) a la que se le pasa como parámetro this. Observe que primero se colocan los directorios y luego los archivos. La infromación de cada elemento la proporciona el método getFileData.
La clase exploraDirectorios implementa las interfaces FilenameFilter y ActionListener. La implementación de FilenameFilter consiste en el método
public boolean accept(File d,String name)
En este método se ha optado por aceptar sólo los archivos con alguna de las extensiones que aparecen en la cadena ext, separadas por espacios. Si ext="*" entonces se aceptan todos los archivos. El método main inicializa ext precisamente como "*". La implementación de ActionListener consiste del método
public void actionPerformed(ActionEvent e).
Aquí lo que se hace es ver si la acción fue generada por el campo de texto que tiene el directorio, por el que tiene las extensiones o por la lista que tiene los datos de los archivos y directorios. Según el caso se actualiza el directorio o las extensiones y se procede a releer el directorio o bien se interpreta cual el el elemento de la lista seleccionado y si éste es un directorio o la primera línea que tiene "..", se procede a hacer cambio de directorio y a leer su contenido.
En esta sección aprovecharemos el trabajo desarrollado en la sección anterior para crear un programa que además de mostranos el contenido de un directorio, nos permita seleccionar un archivo y abrirlo. El programa que desarrollamos se llama exploraArchivos y es una extensión de exploraDirectorios que utiliza además dos clases adicionales desarrolladas específicamente para mostrar textos e imágenes: miraTexto y miraImagen. Ambas clases son extensiones de autoExitFrame.
Éste es el contenido de miraTexto.java.
Observe que lo único que hace esta clase en colocar en su centro un cuador de texto, leer como texto el contenido del archivo cuyo nombre se le pasa como parámetro al constructor y desplegar ese texto en el cuadro de texto.
Éste es el contenido de miraImagen.java.
El objetivo de esta clase es leer la imagen cuyo nombre se le pasa como parámetro en el constructor, investigar sus dimensiones y desplegar la imagen en el marco. Para ello se crea una pizarra (Canvas) especial llamada imageCanvas que contendrá y dibujará la imagen. La lectura de la imagen se facilita con el método getImage() de Toolkit que se encarga de todo el proceso de leer el archivo y convertirlo en una imagen. Este método sólo funciona con archivos de tipo jpg o gif. Para evitar parpadeos mientras se termina la lectura de la imagen, se optó por realizar la lectura de la imagen en un hilo independiente (por eso miraImagen implementa la interfaz Runnable) y por no desplegar la imagen hasta que su contenido está completo, lo cual se logra con el uso del MediaTracker que puede observarse en el método run.
El alumno debe ahora ejecutar el programa exploraArchivos con la llamada:
java unidad08.exploraArchivos
Obtendrá un programa con el mismo aspecto que exploraDirectorios pero con la funcionalidad extra de que al hacer un doble clic sobre uno de los archivos de la lista el archivo "se abrirá". Si se trata de un texto (con extensión .txt o .java) verá su contenido en un marco del tipo miraTexto. Si se trata de una imagen (con extensión .gif, .jpg o .jpeg) verá la imagen en un marco del tipo miraImagen. Si el archivo es una página web (con extensión .htm o .html) el programa tratará de abrirlo con IEXPLORE.EXE. Si el archivo es un ejecutable (con extensión .exe) el programa tratará de ejecutarlo.
Éste es el código de exploraArchivos:
El alumno debe estudiar cómo logra este programa realizar su trabajo. En particular deberá estudiar cómo distingue entre sistemas operativos para hacer la llamada que corresponde para abrir el navegador. Si no está trabajando en Windows o no tiene el navegador IEXPLORE.EXE deberá modificar el código que abre las páginas web y recompilar el programa para que éste funcione en su ordenador.
Esta sección no tiene ejemplos, en ella se trata solamente de recordar y reforzar algunos temas vistos en la unidad 7 del curso Java Inicial. Tanto en la última sección de esta unidad como en la siguiente unidad aparecen varios ejemplos de aplicaciones de corrientes de datos.
El paquete java.io tiene varias clases que son derivadas de dos clases básicas: InputStream y OutputStream y que representan corrientes de entrada de bytes provenientes de una fuente o de salida de bytes que tienen un destino definido, respectivamente. La fuente de una corriente de entrada o InputStream puede ser un archivo, el teclado, un array de bytes en la memoria RAM, una conexión a un URL en internet o muchas otras cosas. El destino de una corriente de salida o OutputStream puede ser un archivo, una impresora, un array de bytes, una conexión a un URL en internet o muchas otras cosas.
Éstos son las variables y los métodos de InputStream y OutputStream:
public abstract class InputStream {
public InputStream(); // vacío
public int available() throws IOException;
public void close() throws IOException;
public synchronized void mark(int límite);
public boolean markSupported();
public abstract int read() throws IOException;
public int read(byte[] b) throws IOException;
public int read(byte[] b,int pos, int n) throws IOException;
public synchronized void reset() throws IOException;
public long skip(long n) throws IOException;
}
public abstract class OutputStream {
public OutputStream(); // vacío
public void close() throws IOException;
public void flush() throws IOException;
public abstract void write(int b) throws IOException;
public void write(byte[] b) throws IOException;
public void write(byte[] b,int pos, int n) throws IOException;
}
Como puede verse estas clases proporcionan los métodos básicos de lectura y escritura y algunos otros que ahora explicamos. available() nos dice el número de bytes que se pueden leer sin bloqueo. Esto quiere decir que son los bytes que se pueden leer sin tener que esperar por más, que están ya disponibles. Este método no debe emplearse para intentar averiguar si una cooriente de datos ya se agotó pues para muchas corrientes de entrada el resultado suele ser siempre cero y eso no quiere decir que ya se llegó al final. Por supuesto close() en ambas clases sirve para cerrar la corriente y liberar los recursos que puedan estarse usando. Si markSupported() regresa true entonces se pueden utilizar los métodos mark(int límite) y reset(), el primero marca la posición de la lectura mientras no se lean más de límite bytes y reset() regresa la lectura a ese lugar, siempre y cuando no se haya excedido el límite de bytes leidos. skip(int n) hace que la lectura prosiga saltándose n bytes. flush() obliga a que se escriba físicamente lo que aún esté en el almacén temporal de escritura cuando se está utilizando un BufferedOutputStream.
Se pueden crear Input y Output Streams a partir de objetos de la clase File. El siguiente ejemplo muestra cómo se crean y se usan estas corrientes. El ejemplo (que ya se presentó en la unidad 7 de Java Inicial) es un programa de línea de comandos que copia un archivo a otro.
DataInputStream y DataOutputStream son abstracciones que nos permiten usar las interfaces DataInput y DataOutput satisfechas en particular por la clase RandomAccessFile, para leer datos de cualquier fuente o escribirlos en cualquier destino. Para poder hacer esto se necesita crear un objeto de la clase InputStream o de la clase OutputStream lo cual se logra gracias a una variedad de métodos de diversas clases que permiten crear InputStreams y OutputStreams en varias situaciones. Luego se pueden usar las clases DataInputStream y DataOutputStream que proporcionan los métodos de lectura y escritura de las interfaces DataInput y DataOutput para cualquier InputStream o OutputStream respectivamente. Esto último se logra gracias a los constructores:
public DataInputStream(InputStream is);
public DataOutputStream(OutputStream os);
Se puede crear un objeto de la clase InputStream con una matriz de bytes como fuente. Para ello se utiliza uno de los constructores de la clase ByteArrayInputStream:
public ByteArrayInputStream(byte[] b);
public ByteArrayInputStream(byte[] b,int pos,int n);
Para leer y escribir cadenas de caracteres (Strings) deben usarse las clases derivadas de Reader y Writer respectivamente pues convierten correctamente bytes a caracteres y viceversa. Por ejemplo, para leer cadenas escritas por el usuario en la línea de comandos, debe crearse un BufferedReader y para ello se convierte el InputStream System.in (que lee bytes) primero en un InputStreamReader que convierte los bytes a caracteres usando la convención local (o la que se le especifique) y luego éste en un BufferedReader que hace la lectura más eficiente y tiene un método readLine() que es que suele usarse para leer cada línea escrita por el usuario cuando éste pulsa intro o return. La línea típica para realizar esta construcción es:
BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
Luego pueden leerse las cadenas que el usuario escribe con líneas como ésta:
System.out.print("escriba su nombre:"); br.readLine();
El ejemplo pipeApplet de la siguiente sección usa un FileReader que realiza una función parecida pero en la lectura desde un archivo. Si lo que se desea es escribir en un archivo de texto lo que conviene usar es un FileWriter.
Si se desea tener un array de bytes como destino de un OutputStream, se puede crear usando los constructores de ByteArrayOutputStream:
public ByteArrayOutputStream();
public ByteArrayOutputStream(int n);
Estos constructores proporcionan un OutputStream al que se puede enviar toda la información que uno desee. El resultado de la construcción es una matriz de bytes (en el segundo caso comienza con un tamaño n) que va creciendo a medida que se necesite y que alberga toda la información que se le ha enviado. La matriz resultante se puede obtener usando el método:
public synchronized byte[] toByteArray();
de la misma clase ByteArrayOutputStream y que da solamente los bytes que se han escrito. El ejemplo pipeApplet de la siguiente sección usa un ByteArrayOutputStream.
También se pueden crear InputStreams y OutputStreams a partir de archivos usando los constructores de la clase FilInputStream y FileOutputStream respectivamente:
public FileInputStream(File fuente);
public FileOutputStream(File destino);
También hay constructores para estas clases partiendo de las direcciones de los archivos en forma de cadenas o partiendo de su FileDescriptor.
Finalmente mencionaremos sólo superficialmente algunas extensiones de InputStream y OutputStream que pueden tener alguna utilidad:
FilterInputStream y FilterOutputStream son subclases de InputStream y OutputStream que sirven para filtrar o transformar los datos. Las clases sólo añaden los constructores:
public FilterInputStream(InputStream is);
public FilterOutputStream(OutputStream out);
Si no se hacen subclases sobreescribiendo los constructores, el resultado es el InputStream o el OutputStream originales. Es precisamente al sobreescribir los constructores que se deben definir los filtros. Por ejemplo se puede definir un filtro que codifique datos de manera que sólo se puedan entender si se descodifican, es decir, si se pasan por el filtro inverso.
Casi todas las otras subclases se construyen como subclases de FilterInputStream y FilterOutputStream para aprovechar los posibles filtros que se deseen utilizar, aunque en la mayoría de las aplicaciones no se usa ningún filtro.
PrintStream es una subclase de FilterOutputStream que implementa una serie de métodos llamados print y println que sirven para escibir cómodamente datos mezclando cadenas con números. Estos métodos convierten todo a cadenas. El alumno ya ha utilizado muchas veces un PrintStream que es System.out, por lo que ya está familiarizado con su funcionamiento. Actualmente la clase PrintStream sólo se usa en System.out, para escribir texto en impresoras o archivos se debe usar siempre Writers: PrintWriter o FileWriter respectivamente.
BufferedInputStream y BufferedOutputStream son subclases que se obtienen usando los constructores
public BufferedInputStream(InputStream is);
public BufferedInputStream(InputStream is,int tamaño);
public BufferedOutputStream(OutputStream is);
public BufferedOutputStream(OutputStream is,int tamaño);
Lo que hacen es usar un "buffer" o almacén temporal con el objeto de mejorar la eficiencia de la lectura o la escritura. En el caso de la escritura es importante llamar al método flush() antes de cerrar la corriente. En los constructores tamaño especifica el número de bytes que se usarán en el buffer.
LineNumberInputStream y PushbackInputStream son otras dos extensiones de FilterInputStream. La primera tiene sólo dos métodos más: int getLineNumber() y void setLineNumber(int L) que permiten llevar control del número de líneas en corrientes de texto. Por supuesto, getLineNumber() devuelve el número de línea en que se encuentra el puntero. setLineNumber(int L) define una nueva numeración de las líneas, lo cual se usa por ejemplo cuando al comenzar una nueva corriente se le quiere asignar el número de líneas de la corriente anterior para numerar las líneas como si provinieran de una sola corriente. PushbackInputStream tiene un solo método público extra: unread(char ch) cuya función es agregar artificialmente a la corriente un caracter extra pecisamente en el lugar donde se encuentra. Esto es útil para convertir archivos de texto de sistemas operativos diferentes donde los pasos de línea pueden estar dados por caracteres diferentes.
SequenceInputStream es una subclase de InputStream que permite concatenar varios InputStreams. Tiene dos constructores:
public SequenceInputStream(InputStream s1,InputStream
s2);
public SequenceInputStream(Enumeration e);
El primero crea un InputStream que consiste en s1 seguido de s2, El segundo tiene como parámetro una enumeración que debe ser de InputStreams y el resultado es un InputStream que recorrerá en orden todos los que vienen especificados en la enumeración.
Antes de terminar esta lección con el estudio de las clases PipedInputStream, PipedOutputStream, PipedReader y PipedWriter, mencionamos otra clase del paquete java.io: StreamTokenizer. Su función es realizar análisis lexicográficos sobre una corriente de bytes. Es parecida a la clase java.util.StringTokenizer que hemos utilizado en algunos ejemplos pero su funcionamiento es más complejo y especializado. No la estudiaremos en este curso.
PipedInputStream y PipedOutputStream son subclases de InputStream y OutputStream que juntas representan las llamadas tuberías (en inglés pipes) de bytes. Estos son los métodos de ambas clases:
public class PipedInputStream extends InputStream
{
public PipedInputStream(PipedOutputStream fuente) throws IOException;
public PipedInputStream(); // vacío
public void close() throws IOException;
public void connect(PipedOutputStream fuente) throws IOException;
public synchronized int read() throws IOException;
public synchronized int read(byte[] b,int pos,int n) throws IOException;
}
public class PipedOutputStream extends OutputStream
{
public PipedOutputStream(PipedInputStream destino) throws IOException;
public PipedOutputStream(); // vacío
public void close() throws IOException;
public void connect(PipedInputStream destino) throws IOException;
public void write() throws IOException;
public void write(byte[] b,int pos,int n) throws IOException;
}
En realidad lo único que hacen estas clases es transformar un InputStream en OutputStream o viceversa. Si un programador desea que la salida de datos de algún proceso se convierta en la entrada de otro proceso basta que cree un par estos objetos de esta manera:
PipedOutputStream pos=new PipedOutputStream();
PipedInputStream pis=new PipedInputStream(pos);
Hecho esto debe asignar como salida del primer proceso a pos y como entrada del segundo a pis. El encadenamiento puede hacerse al revés o creando ambos sin parámetro y usando pis.connect(pos) o pos.connect(pis), da lo mismo.
Análogamente, para hacer tuberías con corrientes de caracteres, se utilizan PipedWriter y PipedReader.
El siguiente ejemplo es un applet en el que se construye una tubería (pipe) que encadena cinco procesos que procesan caracteres usando apareamientos: PipedWriter-PipedReader de la forma:
PipedWriter pw=new PipedWriter();
PipedReader pr=new PipedReader(pw);
Hé aquí el applet. Tiene tres líneas. En la primera hay un campo de texto con una cadena de caracteres, en la segunda hay cinco interruptores y en la tercera hay otro campo de texto donde se obtendrá el resultado del filtro.
Cada vez que se cambia el estado de un interruptor, el applet arranca cinco hilos que se encargan cada uno de hacer (o no hacer) uno de los cambios al texto de arriba. También se arranca un hilo que espera hasta que todos los otros hilos han parado y entonces obtiene el resultado y lo pone en el segundo campo de texto.
Éste es el código del applet:
Para construir este ejemplo se creó una clase abstracta llamada transChars que se encarga de procesar todos los caracteres provenientes de un BufferedReader usando el método abstracto
public abstract char Transform(char b);
y pasar el resultado a un BufferedWriter. Éste es el código de transChars:
También se construyeron seis subclases de transChars que sólo implementan un constructor para determinar su BufferedReader y su BufferedWriter, y el método Transform para realizar su trabajo. copyChars sólo pasa cada caracter sin hacerle nada, quitaAcentos, aMayusculas, aMinusculas, sinVocales y sinConsonantes hacen más o menos lo que sus nombres indican. Aquí está el código de estas seis clases.
Observe que los métodos que hacen las transformaciones de caracteres se programaron usando BufferedReader y BufferedWriter, por lo cual hay que crear objetos de estos tipos a partir de los PipedWriter y PipedReader.
El alumno debe estudiar este ejemplo como un caso típico de un programa que utiliza varias extensiones de una clase abstracta (transChars) y también de un programa que realiza tods sus operaciones en hilos independientes para evitar bloquear a los otros.