jerarquía de clases en java
TRANSCRIPT
Jerarquía de clases en Java
En la jerarquía de clases de la figura aparecen varias clases (faltan algunas). Las más importantes son java.io.FileInputStream,
java.io.ObjectInputStream y java.io.FilterInputStream. La
primera permite leer bytes a partir del flujo de entrada obtenido de un archivo; la segunda permite deserializar datos de tipos primitivos y objetos serializados antes usando java.io.ObjectOutputStream java.io.FilterInputStream es una
clase mucho más misteriosa que las otras: es un decorador. Para no interrumpir la exposición de las clases elementales de E/S, el patrón observador se explicará más adelante. Por ahora, es suficiente con saber que las subclases de
FilterInputStream proporcionan nuevas funciones a las clases a las cuales
"decoran" o "envuelven".Las instancias de las subclases de
FilterInputStream (en la figura faltan
java.io.LineNumberInputStream y java.io.PushbackInputStream)
permiten la lectura de un flujo y la escritura en otro, alterando los datos en el paso de un flujo a otro. Pueden usarse para almacenar datos en buffers, leer tipos primitivos retroceder hacia atrás en un flujo, etc; además, pueden combinarse de modo que la salida de una instancia sea la entrada de otra.
Lectura de un archivo mediante la clase FileInputStream
Un BufferedInputStream usa un array interno de almacenamiento temporal
(buffer) para el flujo de entrada. Cuando se crea un BufferedInputStream,
se creatambién un array interno de almacenamiento de bytes. Cada vez que se
llama a un método read(), los bytes se leen del array de almacenamiento;
según se van leyendo bytes, el array se vuelve a rellenar con bytes del flujo de entrada, leídos de golpe. Así se evita tener que leer y almacenar byte a byte (y llamar cada vez a los métodos nativos del sistema operativo sobre el cual trabaja la máquina virtual Java), lo cual mejora mucho el rendimiento de la E/S. Siempre que se pueda, interesa manejar clases de E/S que trabajen con buffers. Analizemos, como ejemplo, el siguiente código: BufferedInputStream bis = new
BufferedInputStream(objetoInputStream, 1024)
bis.read()
Con el constructor, se ha creado un buffer de 1024 bytes de tamaño. Al llamar
por primera vez a read(), se intentará llenar por completo el buffer a partir del
flujo de entrada. En las siguientes llamadas, se leerá directamente del buffer,
que se irá rellenando, cuando convenga, con los bytes del flujo de entrada.
DataInputStream incorpora métodos para leer bytes y arrays de bytes, al
igualque InputStream; pero además incluye métodos para leer caracteres,
Strings, números (enteros, de punto flotante...), etc. Todos los métodos
empiezan con read: readBoolean(), readByte(), readChar()..., excepto skipBytes()
. La interfaz de la clase DataInputStream
Un objeto FileInputStream (que representa un flujo de entrada que proviene de un archivo)
puede ser decorado por otro BufferedInputStream para proporcionar la capacidad de admitir
buffers (memorias de almacenamiento temporal) y añadir dos nuevos métodos (public void
mark(int limitelectura) y public void reset()). Al crear un objeto de esta clase, la máquina virtual
Java (MVJ) intenta abrir el archivo; en el caso de que no exista, se lanza una excepción
FileNotFoundException. Si no se puede acceder al archivo por motivos de seguridad, se arroja
una excepción SecurityException. En el subapartado dedicado a la clase OutputStream se
pondrán ejemplos del uso de los objetos ObjectInputStream.
A continuación se expone un programa de ejemplo del uso combinado de
BufferedInputStream y FileInputStream, el cual permite leer un archivo
de texto (situado en el directorio donde se ejecuta la clase) y mostrar su contenido en pantalla: LeerCaracter.java
Si el lector prescinde del decorador BufferedOutputStream y decide usar
directamente el método write() de la clase FileOutputStream, notará –
cuando use valores elevados de N– la diferencia de rendimiento ocasionada por no usar buffers.
La jerarquía de clases java.io.OutputStream La superclase raíz java.io.OutputStream proporciona los métodos básicos
para escribir bytes en un flujo de salida basado en bytes (todos ellos son bloqueantes): public abstract void write(int byte) throws IOException
public void write(byte[] datos) throws IOException
public void write(byte[] datos, int offset, int longitud)
throws IOException
El primer método escribe un único byte en un flujo de salida. El segundo escribe
un array de bytes, y el tercero escribe longitud bytes del array datos,
comenzando por la posición indicada por el entero offset.
Los sistemas operativos utilizan buffers internos para evitar tener que escribir los bytes de uno en uno. Así pueden escribir decenas o cientos de bytes de golpe, lo cual redunda en un mejor rendimiento del sistema. Esta clase dispone de un
método (public void flush() throws IOException) que obliga a escribir
todos los bytes que haya en el buffer, esté lleno o no. El tamaño exacto de cada buffer depende del sistema operativo usado y de la implementación de la
máquina virtual Java. OutputStream también dispone de un método public void close()
throws IOException, que se encarga de cerrar el flujo de salida y de liberar
los recursos del sistema usados por el flujo. System.out (el flujo de entrada
estándar) y System.err (el flujo de salida estándar de los errores) son objetos
OutputStream. Más específicamente, son objetos PrintStream.
En la jerarquía de clases derivadas de la superclase base OutputStsream que
aparece en la figura aparecen varias clases (faltan algunas). Las más importantes son java.io.FileOutputStream,
java.io.ObjectOutputStream y
java.io.FilterOutputStream. La primera permite escribir bytes en el flujo
de salida asociado a un archivo; la segunda permite serializar datos de tipos
primitivos y objetos. Como era de esperar, FilterOutputStream es un
decorador. Las instancias de las subclases de FilterOutputStream (en la
figura faltan java.io.LineNumberOutputStream y
java.io.PushbackOutputStream)
permiten decorar los objetos a los que envuelven. DataOutputStream
incorpora métodos para escribir bytes y arrays de bytes, al igual que
OutputStream; pero además incluye métodos para escribir caracteres,
Strings, números (enteros, de punto flotante...), etc. Todos los métodos
empiezan con write: writeBoolean(), writeByte(),
writeChar()...,excepto flush().
FileOutputStream complementa a FileInputStream. Con respecto a esta
última, presenta una diferencia: sus constructores, a diferencia de los de
FileInputStream, no arrojan excepciones del tipo
FileNotFoundException; si el archivo no existe, FileOutputStream lo
crea. En caso de que el archivo sí exista y se utilice el constructor que aparece en la siguiente línea de código: FileOutputStream fos = new FileOutputStream(fichero);
hay que tener en cuenta que, con la primera llamada a write(), los nuevos
datos se escribirán sobre los que ya tenía el archivo, con su consiguiente pérdida. Si se desea que los nuevos datos se añadan tras los ya existentes, se deberá usar este constructor: FileOutputStream fos = new FileOutputStream(fichero, true);
La clase PrintStream incluye varios métodos public void print() y
public void println() para imprimir (en el sentido de mostrar al usuario
por la salida estándar) cualquier valor de un objeto o un tipo primitivo .Los
métodos print() convierten el argumento en un String y luego lo
transforman en bytes de acuerdo con la codificación por defecto del sistema; después estos bytes se escriben del modo descrito para los métodos
write().Los métodos println() hacen exactamente lo mismo que los
print(), pero incluyen un carácter de nueva línea ("\n", "r" o "r\n"; depende de
la plataforma).Los objetos PrintStream presentan una curiosa propiedad con
respecto a lasdemás clases de E/S: no lanzan excepciones. El motivo para este comportamiento reside, probablemente, en que –tal como ya se dijo–, System.out (el flujo de entrada estándar) y System.err (el flujo de salida
estándar de los errores) son objetos PrintStream. Sería bastante incómodo
para un programador tener que escribir código para manejar las excepciones cada vez que escriba líneas inofensivas como
System.out.println("JAVA"). Que esta clase no lance excepciones no
quiere decir que no las pueda producir; lo que ocurre es que las gestiona internamente. Para comprobar si se ha producido algún error se llama al método
public boolean checkError().
Un objeto BufferedOutputStream puede decorar a un OutputStream,
dándole la posibilidad de que los bytes leídos se almacenen en un buffer y se
escriban cuando el buffer esté lleno (salvo que se use flush(), que fuerza a
escribir, esté o no lleno). Su funcionamiento no difiere mucho del correspondiente
a la clase BufferedInputStream, vista en el subapartado anterior: las
llamadas a write()
van almacenando los datos en el buffer, que sólo se escribirá cuando esté lleno
(o se llame a flush()). A continuación, se muestra un ejemplo del uso
combinado de las clases BufferedOutputStream y FileOutputStream, el
cual escribe N veces las letras A y B en un archivo. Escribir Caracter
La clase ObjectOutputStream es una clase muy importante para las
Comunicaciones en red: permite serializar objetos (ObjectInputStream
permite deserializarlos). Serializar es la acción de codificar un objeto en un flujo de bytes; deserializar es la acción de decodificar un flujo de bytes para reconstruir una copia del objeto original. Esencialmente, serializar un objeto equivale a guardar (y luego poder cargar) el estado de un objeto. La serialización es un mecanismo de implementación de la persistencia de objetos. Uso persistencia (de un objeto) en el sentido de capacidad de un objeto para persistir
en el tiempo y en el espacio, independientemente de la MVJ que lo creó. El flujo de bytes que produce la serialización de un objeto se puede enviar a máquinas remotas mediante sockets o se puede guardar en un archivo. Para poder reconstruir correctamente un objeto, el proceso de serialización también almacena en el flujo de bytes la descripción de la clase a la que pertenece el objeto. Bruce Eckel explica así la serialización en Java (Thinking in Java 3rd Edition): La serialización de objetos en Java le permite tomar cualquier objeto que
implemente la interfaz Serializable y convertirlo en una secuencia de bytes que
puede restaurarse completamente más tarde para regenerar el objeto original. Esto es cierto incluso a través de una red, lo que significa que el mecanismo de la serialización compensa automáticamente las diferencias entre sistemas operativos. Esto es, puede crear un objeto en una máquina Windows, serializarlo y enviarlo a través de la red a una máquina Unix donde será reconstruido correctamente. No tiene que preocuparse sobre las representaciones de los datos en las diferentes máquinas, el orden de los bytes o cualquier otro detalle. Por sí misma, la serialización de objetos es interesante porque le permite implementar persistencia ligera. Recuerde que la persistencia significa que el tiempo de vida de un objeto no está determinado por si un programa se está ejecutando; el objeto vive entre las invocaciones del programa. Tomando un objeto serializable y escribiéndolo en el disco, y entonces restaurando ese objeto cuando el programa es reinvocado, puede producir el efecto de persistencia. El motivo por el que se llama “ligera” es que no puede simplemente definir un objeto y usar alguna clase de palabra reservada “persistent” y dejar que el sistema se preocupe de los detalles (aunque esto podría ocurrir en el futuro). En lugar de eso, debe explícitamente serializar y deserializar
los objetos en su programa. Una característica sumamente interesante de la serialización estriba en su capacidad de congelar objetos vinculados y de hacer que retornen a su estado original, aunque la máquina de destino esté a miles de kilómetros de la maquina donde se crearon originalmente los objetos. Cuando se serializa un objeto, se serializan también todos los objetos a los que tenga referencias (los cuales deben, por tanto, implementar también la interfaz
Serializable); como es lógico, al deserializarlo se reconstruyen también los
objetos vinculados. Esta propiedad abre posibilidades muy interesantes para los programadores: mareas enteras de objetos interconectados de muchas maneras pueden almacenarse y recuperarse cuando se necesiten. Si se intentará simular
la serialización de un objeto mediante la clase DataOutputStream,
seprecisaría guardar cada dato de tipos simples (int, double, float...)
contenido en el objeto, así como los datos de tipos simples que contuvieran los objetos a los cuales contiene referencias. Se puede trabajar así, pero sería tarea muy tediosa y propensa a errores.Cuando se deserializa un flujo de bytes, se
llama al método protected Class resolveClass(ObjectStreamClass descripcion) throws
IOException,
ClassNotFoundException de la clase ObjectInputStream para que
cargue dinámicamente los bytecodes de las clases cuyas descripciones encuentra en el flujo (suponiendo que no los hubiera cargado antes). Este método llama al cargador de clases de Java, el cual es el verdadero encargado
de buscar los archivos .class
necesarios y de cargar dinámicamente sus bytecodes. El cargador de clases busca, en primer lugar, en el directorio actual (aquel desde el cual se ejecutó la aplicación), y si no los encuentra sigue buscando en los directorios indicados en
el CLASSPATH. Si finalmente no encuentra los .class que se necesitan, lanza
una excepción java.lang.ClassNotFoundException. Siempre interesa
configurar el CLASSPATH
local para que incluya todos los directorios donde estén los archivos .class de
las clases que se necesitarán durante el proceso de deserialización, pues no es habitual que estén todos en el directorio desde el cual se lanza la aplicación. La mejor manera de ver cómo funciona la serialización es mediante un ejemplo
completo. En él veremos cómo se graban objetos Persona en un archivo
llamado personas.txt (si no existe, se crea), ubicado en el directorio donde
se ejecuta EscribirPersona.
Escribir Persona
public String toString() { return ("Nombre: " + nombre +"; Edad: " + edad); }
}
Persona implementa la interfaz Serializable porque, tal como se dijo
cualquier objeto que pueda ser serializado (y deserializado) debe implementarla. Leer Persona
} }
Para que el código funcione correctamente, se necesita ejecutar LeerPersona
desde el mismo directorio donde se ejecuta EscribirPersona o configurar
el´CLASSPATH para que LeerPersona tenga acceso al archivo
Persona.class (generado al compilar la clase EscribirPersona), y
después de ejecutar esta última al menos una vez.
Jerarquía de Clases (sigue)
No hay ningún obstáculo para serializar a través de redes. A continuación incluyó el código de la versión de red correspondiente al ejemplo anterior Tal y como está escrito, se considera que el cliente y el servidor se ejecutan en
la misma máquina. Si se desea ejecutar EscribirPersonaRed en una
máquina distinta de aquella donde se ejecuta LeerPersonaRed, habrá que
modificar la línea Socket socket = new Socket("localhost", 9000);
y escribir, en lugar de localhost, el nombre de la máquina donde se vaya a
ejecutar LeerPersonaRed. Además, será necesario colocar el archivo
Persona.class en esta última máquina de manera que LeerPersonaRed
pueda acceder a ella cuando se ejecute. EscribirPersonaRed.java
Persona.java
LeerPersonaRed.java