El código es muy tonto, pero es extensible a otros casos más complejos. En general, se trata de leer de un InputStream o Reader y, tras un procesado o no, escribir lo leído en un OuputStream o Writer. Veamos el ejemplo en cuestión:
BufferedInputStream in = new BufferedInputStream(new FileInputStream(archivoOrigen));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(archivoDestino));
byte buffer[] = new byte[1024];
while(in.read(buffer,0,1024)!=-1){
out.write(buffer);
}
in.close();
out.close();
Simple, ¿verdad? Leemos los bytes de un fichero a través del stream in, de 1024 en 1024 y lo vamos escribiendo en otro fichero a través del stream out. Vamos lo que viene siendo copiar un fichero.
¿Dónde está el error? Veamos lo que dice el javadoc de la función read: devuelve el número de bytes leídos o -1 si se ha alcanzado el final del fichero. Es decir, mientras haya datos los va a leer, los va a almacenar en buffer y nos va a devolver el número de bytes leídos. Como mucho, leerá 1024 cada vez, ya que lo hemos configurado así. Nosotros estamos ignorando el valor devuelto y de ahí nuestro error.
En este ejemplo tan tonto, se pueden dar dos errores distintos:
Error #1
Lo normal es que en cada llamada al metodo read se lean 1024 bytes, pero cuando llegamos al final del fichero, es decir, en la penúltima lectura, se leerán tan sólo los bytes restantes -recuerda que en la última lectura no se lee nada, se detecta fin de fichero, se devuelve -1 y se termina el bucle while-.
Por ejemplo, supongamos que al final quedan 534 bytes por leer. Vemos el código paso a paso:
- while(in.read(buffer,0,1024)!=-1){
- out.write(buffer);
Esto provoca que el fichero copiado no sea exactamente igual que el leído, ya que el copiado tiene unos bytes de más, concretamete 1024 menos el resto de dividir el tamaño del fichero por 1024. Dicho de otra forma, el fichero generado siempre va a tener un tamaño multiplo de 1024, sea cual sea el tamaño del leído.
El error en este ejemplo sería fácil de detectar ya que se trata de copiar un fichero. Si miramos los tamaños de los ficheros y no son iguales es que falla. Sin embargo, si hacemos un procesado de los bytes, por ejemplo el redimensionamiento de una imagen, detectar el error es más complicado. Por un lado, los tamaños de los ficheros origen y destino no tienen por qué coincidir, y por otro, puesto que los bytes se añaden al final y en general serán pocos comparados con el resto, tan sólo van a suponer un cierto número de píxeles de diferencia, cosa que puede no apreciarse en la imagen o achacarse a efectos del procesado.
Error #2
Otro error que puede ocurrir es cuando en el flujo de entrada in es lento. Por ejemplo, supongamos que estuvieramos leyendo a través de internet, mediante http, un fichero. Podría ocurrir que en una iteración el flujo in no tuviera datos que leer. En este caso, no almacenaría nada en el buffer y devolvería 0 como resultado. Sin embargo, nosotros sí que escribiríamos el buffer, que contiene los datos de la lectura anterior, en el flujo out, repitiendo de esta forma el conjunto de bytes en el fichero de destino. Es decir, estaríamos escribiendo dos o más veces los mismos bytes.
Esto me ocurrió en un pequeño programita que leía los bytes de una página web y los enviaba por correo electrónico. Al repetirse ciertas secuencias de bytes, el html leído no era para nada correcto: a veces había dos etiquetas <html><head>.... , otras veces había dos <body>, y lo peor es que era aleatorio. Unas veces ocurría una cosa, otras veces otra distinta y a veces leía bien. Por supuesto, se debía a que a veces la conexión era lenta y se quedaba atascado en un sitio, otras veces en otro y otras iba bien. Cuando se quedaba atascado, el bucle iba repitiendo el último trozo leído. Un verdadero quebradero de cabeza, os lo aseguro.
Solución
La solución a esto es bien sencilla. Simplemente basta con almacenar el número de bytes leídos y escribir solamente este número usando la función write con 3 parámetros en lugar de 1:
BufferedInputStream in = new BufferedInputStream(new FileInputStream(archivoOrigen));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(archivoDestino));
byte buffer[] = new byte[1024];
int leidos;
while((leidos=in.read(buffer,0,1024))!=-1){
out.write(buffer,0,leidos);
}
in.close();
out.close();
Por último, alguno pensará que por qué no se leen y escriben todos los bytes del tirón en lugar de hacerlo de 1024 en 1024. La respuesta es que se debe a cuestiones de rendimiento: se hace para evitar tener alamacenado en memoria todo el fichero. De esta forma, aunque leas un fichero de cientos de megas únicamente tendrás a la vez en memoria 1024 bytes del fichero. Por supuesto, el tamaño del buffer cada uno lo puede ajustar a sus necesidades. Si hay que procesar el fichero, puede que no haya más remedio que mantenerlo todo en memoria. La cuestión es evitarlo en la medida de lo posible.
Espero que os haya servido.