G.Bordel >Docencia >TAP Técnicas Actuales de Programación (curso 2010-2011)
desprotegido Intro. desprotegido Temario desprotegido Calendario desprotegido RPF desprotegido Recursos protegido Práctica protegido Gest. Alum.
tema_anterior Tema 9: Entrada y salida de datos tema_siguiente
  1. Introducción.
  2. Estructura de clases para E/S.[ejercicios]
  3. E/S de objetos. Serialización.
  4. Creación de nuevas clases de E/S.[ejercicios]

9.2- Estructura de clases para E/S

Dentro de java.io encontramos algunas clases de interés además de todas aquellas que conforman el núcleo básico de lo que es entrada/salida en Java. Nos centraremos en primer lugar en este núcleo básico y dejaremos par el final las otras clases.

El mencionado núcleo básico está formado por cuatro árboles de clases que podemos representar gráficamente como en la siguiente figura, ordenados según dos criterios: por un lado entrada-salida y por otro byte-caracter, de modo que los cuatro árboles se corresponden con las 4 combinaciones posible de estos dos elementos: entrada de bytes, entrada de caracteres, salida de bytes y salida de caracteres.

La diferenciación entre bytes y caracteres viene forzada por el hecho de que los caracteres en Java se codifican en UNICODE, por lo que son elementos de 16 bits (si fuesen de 8 bits podria reducirse todo a entrada y salida de bytes y su adecuada interpretación como caracteres en el caso necesario).


Las clases que tratan con "streams" se agrupan en cuatro árboles y dentro de cada uno hay dos tipos de funcionalidades: conexión con origenes y destinos y procesammientos (la representación gráfica no refleja relaciones de herencia reales).

Cada uno de estos árboles, a su vez, puede considerarse compuesto por dos tipos de clases, relacionadas unas con origenes o destinos de datos, y otras con tipos de procesamiento. De este modo se separan las funcionalidades de los origenes y destinos pudiendose combinar unas con otros. Las clases con capacidad de procesamiento son "wrappers" (envoltorios, un patrón de diseño software) que se aplican a las clases de origen o destino proporcionando sus capacidades al origen o destino que envuelven (se verán ejemplos más adelante).


Árbol de clases para entrada de bytes.

Árbol de clases para salida de bytes.

Árbol de clases para entrada de caracteres.

Árbol de clases para salida de caracteres.

A la izquierda vemos los 4 árboles en donde, con fondo gris se encuentran las clases ligadas a fuentes y destinos de datos y con fondo blanco las clases que realizan algúnu tipo de procesamiento. Todas ellas se comentarán más adelante. Prestaremos atención en este momento a las cuatro clases raíz de los árboles.

Estas clases proporcionan el método básico para entrada (int read()) o salida (int write(int)) de un byte o caracter. Como puede verse, estos métodos tienen una particularidad: no trabajan con "bytes" o "chars", sino con "int" de modo que deben realizarse las conversiones pertinentes.

Además de estos métodos básicos, se dispone de lectura y escritura de bloques sobre arrays: int read(byte[]), int read(char[]), int write(byte[]) e int write(char[]), donde el número de elementos a leer o escribir viene dado por la dimensión del array.

Por último un tercer tipo de método permite leer o escribir dentro de una zona de un array: int read(byte[],int,int), int read(char[],int,int), int write(byte[],int,int) e int write(char[],int,int), donde el primer parámetro entero es el "offset" dentro del array donde se comenzara a actuar y el segundo es el número de elementos a utilizar.

A continuación veremos la funcionalidad de cada una de las clases que contienen estos cuatro árboles, pero comenzamos por ver cómo hay una regularidad en la denominación de las clases que nos facilita su memorización: todas las clases que tratan con bytes se "adueñan" del término "stream" denominandose XxxInputStream o XxxOutputStream dependiendo de que sean de entrada o de salida, donde Xxx representa bien la denominación del origen o destino de datos o bien su funcionalidad; por otro lado las clases que tratan con caracteres se denominan XxxReader o XxxWriter en función de que sean de entrada o de salida y del mismo modo y coincidiendo con el otro caso, Xxx es la denominación del origen o destino de los datos.

En la siguiente tabla pueden verse las clases de entrada y salida de los origenes y destinos básicos. Estos orígenes y destinos son de tres tipos: estructuras de memoria, "pipes" (tuberias entre procesos) y ficheros. Tanto en el caso de pipes como de ficheros, la regularidad es total disponiendo de las 4 clases necesarias (Reader,Writer,InputStream y OutpuStream). Por el contrario en el caso de la memoria existen diferencias. Las Strings pueden ser leidas y escritas mediante caracteres, y existe una clase para leer StringBuffers como bytes pero no debe ser utilizada (se mantiene para seguir soportando antiguos programas que la utilizaran). Además de esto se puede acceder a array tanto en lectura como en escritura con la particularidad ievidente de que los streams actuarán sobre ByteArray y los Reader y Writer sobre CharArray.


Clases de entrada y salida de los origenes y destinos básicos.

En el caso de las clases que procesan datos las regularidades son menores. A continuación vemos la tabla y comentaremos las particularidades que procedan en cada caso


Clases para entradas y salidas con procesamiento de datos.

Conversión de bytes a caracteres.
En ocasiones podemos recibir un objeto de tipo "stream" (que hace e/s con bytes) del que sabemos que está realmente actuando sobre texto (caractéres), de modo que lo correcto sería disponer de un Reader o un Writer. Para cubrir esta necesidad se dispone de las clases InputStreamReader y OutputStreamReader. Disponemos de un ejemplo muy simple con la entrada estándar: si no ha sido redireccionada sabemos que se refiere al teclado y por tanto recoge caracteres, y sin embargo el objeto que se nos proporciona -System.in- es un InputStreamReader (para no limitar las posibilidades de redireccionamiento), de modo que normalmente envolveremos a este objeto para convertirlo en un Reader.

1-  InputStreamReader entrada=new InputStreamReader(System.in));

Buffering.
Las clases básicas -las cuatro raices de los árboles de clases mencionados- proporcionan entradas y salidas de un byte o un caracter, que serán las utilizadas en caso de las utilicemos directamente para ser envueltas con las clases de procesamiento que vayamos a utilizar. Habitualmente, por razones de eficacia computacional, es aconsejable disponer de un buffer intermedio entre la fuente o destino de datos y nuestro programa de modo que se hagan lecturas o escrituras de bloques de bytes o caracteres (tambien proporcionadas por las clases básicas como se ha visto). Además de este motivo, en el caso de la lectura de caracteres tenemos que nos proporciona un método de utilidad básico: la lectura de lineas (readLine()). De nuevo podemos fijarnos en la entrada estándar:

1-  InputStreamReader entrada=new InputStreamReader(System.in));
2-  BufferedReader entradaB=new BufferedReader(entrada);
3-   //....
4-  String s=entradaB.readLine();
o directamente:
1-  BufferedReader entradaB=new BufferedReader(new InputStreamReader(System.in));
2-   //....
3-  String s=entradaB.readLine();

Filtrado.
Las cuatro clases para filtrado son simplemente clases que actuan como raiz de otras que lleven a cabo filtrados específicos. En concreto las que acabamos de comentar con anterioridad (las de inclusión de un buffer) son clases hijas de las de filtrado. Si precisamos escribir nuevas clases que realicen un determinado proceso sobre las entradas o salidas, lo razonable es extender estas clases o algunas de sus descendientes. (en un apartado próximo veremos un ejemplo de programación de un par de clases de entrada salida propias que extenderán estas).

Concatenación.
La clase SequenceInputStream permite leer bytes de una serie de fuentes como si se tratase de una sola, encargandose de cerrar una y abrir la siguiente cuando es preciso. (esta necesidad aplicada al caso de caracteres en lugar de bytes justifica también la necesidad de las clases de conexión entre bytes y caracteres como se ve en el siguiente ejemplo).

1-  //procesa los ficheros "texto1" y "texto2"
2-  BufferedReader entradaB=new BufferedReader(
3-                          new InputStreamReader(
4-                          new SequenceInputStream(
5-                                   new FileInputStream("texto1"),
6-                                   new FileInputStream("texto2")
7-                                   )));
8-  String s;
9-  while ((s=entradaB.readLine())!=null) {//procesamiento de s};

Conversión de datos.
Las clases DataInputStream y DataOutputStream proporcionan métodos para leer y escribir toda clase de elementos primitivos como tal, es decir en modo binario (no textual).

Conteo.
Esta es una utilidad muy simple que consiste únicamente en proporcionar la capacidad de controlar el número de línea en que se encuentra la lectura. Es una extensión de las clases con buffer (por lo que se dispone de lecturas de líneas). Esta funcionalidad por tanto sólo tiene sentido con textos y la clase para bytes -LineNumberInputStream- ha dejado de ser de utilidad desde que apareció la de caracteres -LineNumberReader- (los streams existen desde el primer momento, la versión 1.0, mientras que Readers y Writers aparecieron en la 1.1).

Peeking ahead.
Tambien esta es una utilidad muy puntual consistente en la capacidad de dar por no leido un caracter o byte después de haberlo hecho, de modo que la siguiente lectura vuelva a proporcionarlo. Se trata de una utilidad de uso frecuente en el procesamiento secuencial de datos (p.ej. en el "parsing" de textos que se ajustan a lenguajes formales como es el caso de la compilación para los lenguajes de programación) dado que en ocasiones se localiza un item al comenzar a leer el siguiente y por tanto se da por no leido lo último y se procesa el item encontrado.

Printing.
las clases para impresión de datos aportan principalmente los métodos print y println admitiendo como parámetros todos los tipos de datos primitivos y objetos. Un objeto de este tipo ya es conocido para nosotros: el System.out que utilizabamos en el programa "HolaMundo" es un PrintStream.

Serialización de objetos.
Una gran utilidad de Java es la posibilidad de enviar y recibir objetos a través de las entradas y salidas. Para ello se aplica el mecanismo de "serialización" que forma una serie de bytes con la información del objeto de modo que posteriormente permite reconstruirlo. El siguiente apartado se dedica integramente a este tema.

Otras clases en java.io

File

Esta clase representa a los ficheros del sistema, tanto archivos como directorios, y proporciona métodos para realizar operaciones relativas al sistema de ficheros (crear directorios, determinar las raices, etc). Un objeto de esta clase será una representación de una entrada en el sistema de ficheros y permite obtener información referente a ella (tamaño, fecha de creación, etc) y determinar su situación (path canónico, prefijo, absoluto) y en todo caso con independencia de las particularidades del sistema operativo (distintos separadores).

StreamTokenizer

Esta clase es, en principio, similar a la ya conocida StringTokenizer con la diferencia de que en este caso el objeto a trocear es cualquier clase de Stream de entrada. Esta funcionalidad similar esta ampliada con la capacidad específica de reconocer elementos propios de los lenguajesde programación como identificadores, números y comentarios.

RandomAccessFile

Esta clase permite la entrada y salida de datos frente a un fichero de un modo que podemos considerar no secuencial, ya que al disponer del método seek que permite situarse en un punto cualquiera del mismo, pueden hacerse accesos aleatorios. La clase dispone de métodos de lectura y escritura de todos los métodos primitivos.

Siguiente punto: 9.3- E/S de objetos. Serialización


Plataforma de soporte a curso y contenidos (c) German Bordel 2005.