MokaPackages
I packages del JDK
java.util.zip e java.util.jar
di
Antonio
Cisternino
Impariamo a comprimere i nostri dati

 



In questo articolo faremo un breve viaggio attraverso i due package offerti dal JDK per la compressione dei dati: il package java.util.jar e il package java.util.zip

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.

 


MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it