Introduzione
In
questo articolo prenderemo in considerazione due package non molto conosciuti
dal JDK: il package java.util.jar e il package java.util.zip. Questi due
package offrono la possibilità di poter utilizzare formati standard
di compressione nelle proprie applicazioni e quindi leggere e scrivere
file nei formati ZIP, GZIP e JAR. Il formato ZIP è quello più
diffuso nel mondo Windows fin dallo storico PKZIP della PKWARE per MS-DOS.
Il formato GZIP è invece diffusissimo nel mondo Unix.
Entrambi
i formati si basano sullo stesso algoritmo di compressione chiamato deflate.
Ci sono però notevoli differenze nei due formati: il primo consente
di memorizzare più file eventualmente organizzate in più
directory; il secondo si limita a comprimere un singolo file, successivamente
utilizzando il programma tar consente di produrre un file a partire da
più file eventualmente organizzati in directory. Questo spiega perché
i file GZIP hanno spesso estensione .tar.gz oppure .tgz sono dei file prodotti
col tar e successivamente compressi usando gzip. Il formato JAR è
una variante del formato ZIP in cui è opzionalmente contenuto un
file di manifesto che contiene informazioni sui dati contenuti ed utilizzato
da Java per firmare le applicazioni memorizzate in un file JAR.
In
questo articolo accenneremo al problema della codifica dell'informazione
e quindi a come sia possibile la sua compressione. Successivamente considereremo
le classi offerte dai due package per il trattamento dei dati compressi.
La codifica dell'informazione
La
compressione dei dati è uno degli aspetti fondamentali della gestione
dei dati in Informatica. La teoria dell'informazione, sviluppata principalmente
da Claude Shannon negli anni cinquanta, contiene i fondamenti teorici che
rendono possibili la compressione
dell'informazione.
Questa teoria definisce formalmente il concetto di informazione e, attraverso
lo studio dell'entropia dell'informazione e dei codici univocamente decifrabili,
stabilisce il limite massimo alla comprimibilità dell'informazione.
Si occupa inoltre dello studio dei codici correttori, ovverosia di quei
sistemi di codifica che attraverso la ridondanza consentono di rilevare
degli errori nei dati ed eventualmente correggerli.
Un
sistema di codifica è costituito da un insieme di simboli detto
alfabeto. Ad esempio per l'italiano il sistema di codifica prevede che
vi sia un alfabeto contenente le lettere. Nel caso dei numeri l'alfabeto
comprende le cifre da 0 a 9.
Il
codificatore si occupa di codificare un messaggio utilizzando i simboli
dell'alfabeto. Ad esempio chi scrive codifica le parole di una frase in
lettere. Una volta codificata l'informazione può essere ad esempio
trasmessa ad un decodificatore che si occuperà di invertire il procedimento.
Esistono
sistemi di codifica più o meno efficaci e le tecniche di compressione
si occupano di eliminare eventuali ridondanze nella codifica e ridurre
così l'effettiva occupazione dei dati.
L'idea
di base della compressione è quella della codifica a lunghezza variabile.
Nella rappresentazione usuale dei caratteri si usano otto bit per rappresentare
un carattere. Questa scelta non è la migliore dal punto di vista
dell'occupazione: ci sono caratteri utilizzati raramente (si pensi ad esempio
alla lettera k e alla sua occorrenza in un testo) mentre altri sono utilizzati
frequentemente (come ad esempio la lettera a). L'idea è quindi di
utilizzare una codifica in cui i caratteri abbiano una lunghezza variabile
e non costante di otto bit. Si potrebbe ad esempio utilizzare il singolo
bit 1 per rappresentare la lettera a riducendo così ad un ottavo
lo spazio necessario alla sua memorizzazione. Ovviamente il codice a lunghezza
variabile comporterà la presenza di caratteri con una codifica lunga
più di otto bit. In media la lunghezza di un testo codificata in
questo modo sarà più breve di quella di una codifica standard
a otto bit.
Definendo
una misura dell'informazione (il bit) e il concetto di entropia di una
sorgente di informazione è possibile dimostrare che esiste un limite
inferiore alla lunghezza minima della codifica di un messaggio. Quindi
non è possibile inventarsi codici sempre più furbi per ridurre
a piacere la dimensione dei dati. Questo spiega perché quando si
comprime un file già compresso non si ottengono ulteriori benefici
e qualche volta il file cresce semplicemente in dimensione di qualche byte.
Un
algoritmo di compressione è quindi un algoritmo che trasforma i
dati da una codifica, come ad esempio quella a otto bit, ad un'altra che
risulta essere più corta. Il corrispondente algoritmo di decompressione
prederà i dati in formato compresso e li riporterà nella
loro codifica originale.
Quanto
detto finora vale per la compressione lossless, ovvero senza perdita di
informazione. Nel caso della codifica di audio e video la perdita di piccole
quantità di informazione non comporta grossi problemi per la fruizione
dell'informazione e si adottano tecniche di compressione lossy dove si
ammette che il dato decodificato sia solo un'approssimazione di quello
codificato. In questo modo si possono raggiungere dei fattori di compressione
decisamente superiori.
Verificare i dati
Abbiamo
detto cosa significhi comprimere i dati e accennato a quale sia una possibile
tecnica di compressione (che tra l'altro è impiegata nei formati
ZIP e GZIP), cerchiamo ora di capire cosa siano i codici a controllo di
errore e a cosa servano. Il problema è sostanzialmente quello
di garantire che un messaggio arrivi da un mittente ad un destinatario
senza essere alterato dal canale di comunicazione senza che il secondo
se ne accorga. In questo caso quindi il problema consiste nel trasmettere
il messaggio più una parte addizionale che consenta di rilevare
eventuali errori che si siano prodotti nella trasmissione del messaggio.
Utilizzando
tecniche matematiche è possibile costruire dei codici in grado di
rilevare un certo numero di errori in un messaggio ed eventualmente in
grado di correggerli. L'idea è quella di costruire dei codici ridondanti
in cui la ridondanza viene sfruttata per individuare ed eventualmente recuperare
gli errori. Si pensi ad esempio di codificare un bit con tre bit: se vale
1 si trasmette 111 se vale 0 si trasmette 000. Se chi riceve 010 sa che
potrebbe trattarsi di una sequenza 000 in cui si è verificato un
errore.
Un
altro modo per controllare l'integrità di un messaggio è
quello di calcolare una funzione a partire dal messaggio ed ottenere un
valore che viene trasmesso. Al momento della ricezione del messaggio viene
ricalcolata la funzione e viene confrontato il risultato con quello ricevuto,
se i due coincidono non vi sono stati errori nella trasmissione con una
certa
probabilità. Se la probabilità è sufficientemente
piccola si può assumere che non vi siano stati errori. Una funzione
di questo tipo è quella che produce il CRC (Cyclic Redundancy Check)
del messaggio composto tipicamente da quattro bytes. Un altra funzione
è quella che calcola l'Adler32 del messaggio, l'idea è sempre
la stessa ma cambia il modo di produrre i quattro byte.
Nei
formati che prenderemo in considerazione vengono impiegati CRC a quattro
byte e Adler32 a quattro byte per verificare che i dati non siano corrotti.
In questo modo l'overhead necessario diviene di soli quattro byte. Inoltre,
nel caso del formato ZIP, si usa un CRC per ogni file memorizzato in modo
da recuperare tutti i dati non corrotti dal file compresso.
Il package java.util.zip
Questo
è il package più importante per la gestione dei dati in formato
compresso. Consente sia di leggere e scrivere file in formato ZIP e GZIP
che utilizzare stream compressi per il trasferimento dei dati su file o
attraverso la rete.
La
classe base del package è la classe Deflater che implementa l'algoritmo
di compressione alla base del formato ZIP e del formato GZIP (Lempel Ziv).
In realtà il formato ZIP ammette diversi tipi di compressione che
non usano questa tecnica di compressione ma questa è sicuramente
la più utilizzata. La classe Inflater si occupa invece di implementare
l'algoritmo per la decompressione dei dati compressi utilizzando la classe
Deflater. Queste due classi non si usano direttamente se non in casi eccezionali.
Sono
poi definite le due classi Adler32 e CRC32 che implementano gli algoritmi
per il controllo di integrità dei dati. Utilizzando una di queste
due classi è possibile creare un CheckedInputStream o un CheckedOutputStream
ovverosia un input stream oppure un output stream di cui viene calcolato
il checksum utilizzando l'algoritmo CRC32
oppure
Adler32.
Un
esempio che calcola il CRC32 per un file è il seguente:
import
java.util.zip.*;
import
java.io.*;
public
class test() {
public static void main(String[] args) throws Exception {
FileInputStream f = new FileInputStream("FileDiProva.txt");
CheckedInputStream in = new CheckedInputStream(f, new CRC32());
while (in.available() > 0) {
in.read();
}
System.out.println("Checksum CRC32 is "+in.getChecksum().getValue());
}
}
Ci
sono poi varie coppie di stream di input e di output che eseguono la compressione
utilizzando un certo formato. Gli stream di input decomprimono i dati mentre
quelli di output li comprimono. Il DeflaterOutputStream comprime i dati
utilizzando il formato Deflate che vengono poi decompressi utilizzando
l'InflaterInputStream. La coppia GZIPOutputStream e GZIPInputStream si
occupa di comprimere e decomprimere i dati utilizzando il formato GZIP
mentre la coppia di stream ZipOutputStream e ZipInputStream si occupa del
formato ZIP.
Se
ad esempio si vogliono scrivere dei dati in formato GZIP si procede come
segue:
import
java.util.zip.*;
import
java.io.*;
public
class test {
public static void main(String[] args) throws Exception {
FileOutputStream f = new FileOutputStream("out.gz");
GZIPOutputStream outc = new GZIPOutputStream(f);
PrintWriter out = new PrintWriter(outc);
out.println("Questi sono dati in formato compresso GZIP");
out.println("Fine del test");
out.close();
}
}
Osservare
come gli stream compressi possano essere combinati con gli altri nello
stile di Java e del package java.io. Se il risultato della nostra
prova può essere decompresso con un programma che supporta il formato
GZIP non si può dire la stessa cosa se lo stesso esempio viene fatto
utilizzando gli stream ZIP. I dati sono infatti compressi ma non sono leggibili
con un programma che legge i file in formato ZIP. Questo è dovuto
al fatto che il formato ZIP può contenere più file e quindi
non contiene solo dati compressi ma anche le
informazioni
relative al loro nome. In questo caso è necessario utilizzare la
classe ZipFile per gestire un file in formato ZIP. Inoltre sarà
necessario utilizzare la classe ZipEntry che descrive un file all'interno
del file ZIP. Il seguente esempio stampa i file contenuti in un file ZIP:
import
java.util.zip.*;
import
java.util.*;
import
java.io.*;
public
class test {
public static void main(String[] args) throws Exception {
ZipFile f = new ZipFile("prova.zip");
for (Enumeration e = f.entries(); e.hasMoreElements();) {
ZipEntry zf = (ZipEntry)e.nextElement();
if (zf.isDirectory())
System.out.println("Directory "+zf.getName());
else
System.out.println("File "+zf.getName());
}
f.close();
}
}
Sempre
utilizzando la classe ZipFile è possibile creare file in formato
ZIP.
Il package java.util.jar
Il
formato JAR, come già detto, è basato sul formato ZIP ma
prevede che possa essere contenuto un file di Manifesto che contiene informazioni
che riguardano i file contenuti nell'archivio compresso. Ecco quindi che
le classi JarFile, JarEntry, JarInputStream e JarOutputStream sono analoghe
alle classi del package java.util.zip col prefisso Zip al posto del prefisso
Jar; queste classi sono infatti derivate da quelle per il formato ZIP.
Il
package contiene anche la classe Manifest che contiene dati come ad esempio
la firma del file Jar per la sicurezza. Il malnifesto di un file JAR è
un insieme di coppie nome valore chiamati attributi che possono essere
analizzati utilizzando i metodi della classe. Gli attributi sono descritti
utilizzando le classi Attributes e Attributes.Name.
Nella
documentazione del JDK si trovano ulteriori informazioni relative al manifesto
del file JAR e al suo contenuto.
Conclusioni
In
questo articolo abbiamo analizzato i due package java.util.zip e java.util.jar
dopo aver fatto una breve introduzione al significato della compressione
e della codifica.
|