JAVA中的壓縮與解壓縮
2007-10-13 00:58
本文通過對數據壓縮算法的簡要介紹,然后以詳細的示例演示了利用java.util.zip包實現數據的壓縮與解壓,并擴展到在網絡傳輸方面如何應用java.util.zip包現數據壓縮與解壓

綜述

許多信息資料都或多或少的包含一些多余的數據。通常會導致在客戶端與服務器之間,應用程序與計算機之間極大的數據傳輸量。最常見的解決數據存儲和信息傳送的方法是安裝額外的存儲設備和擴展現有的通訊能力。這樣做是可以的,但無疑會增加組織的運作成本。一種有效的解決數據存儲與信息傳輸的方法是通過更有效率的代碼來存儲數據。這篇文章簡要的介紹了數據的壓縮與解壓縮,并展示了用java.util.zip包來實現數據的壓縮與解壓縮是多么的方便與高效。

當然用諸如WinZip,gzip,和Java壓縮(或jar)之類的工具也可以實現數據的壓縮與解壓縮,這些工具都是獨立的應用程序。你也可以在JAVA應用程序中調用這些工具,但這并不是最直接的方法,也不是有效的解決方法。尤其是你想更快速地實現數據的壓縮與解壓縮(例如在傳輸數據到遠程機器之前)。這篇文章包括以下內容:

  • 給出一個關于數據壓縮的簡單的介紹
  • 描述java.util.zip包
  • 示例如何使用該包實現數據的壓縮與解壓縮
  • 示例如何壓縮串行化的對象并將其存儲在磁碟上
  • 示例如何通過數據壓縮來增強"客戶/服務"應用程序的性能

數據壓縮概述

文件中數據冗余的最簡單的類型是"字符的復制"。讓我們先來看下面一個字符串:

  • JJJJJJAAAAVVVVAAAAAA
    這個字符串可以用更簡潔的方式來編碼,那就是通過替換每一個重復的字符串為單個的實例字符加上記錄重復次數的數字來表示,上面的字符串可以被編碼為下面的形式:

  • ***A4V6A
    在這里,"6J"意味著6個字符J,"4A"意味著4個字符A,以此類推。這種字符串壓縮方式稱為"行程長度編碼"方式,簡稱RLE。

再舉一個例子,考慮一下矩形圖像的存儲。一個單色位圖,可以被存儲為下面這種形式,如圖1所示。


圖1:RLE方式下的位圖信息

另外一種方式是將圖像存為一個圖元文件:

Rectangle 11, 3, 20, 5

上面的表示方法是講矩形的起始坐標是(11,3),寬度是20,高度是5。

上述的矩形圖像可以使用RLE編碼方式壓縮,通過對相同位記數表示如下:

0, 40
0, 40
0,10 1,20 0,10
0,10 1,1 0,18 1,1 0,10
0,10 1,1 0,18 1,1 0,10
0,10 1,1 0,18 1,1 0,10
0,10 1,20 0,10
0,40

上面第一行是講圖像的第一行由40個0組成。第三行是講圖像的第三行是由10個0加上20個1再加上10個0組成,其它行以此類推。

大家注意,RLE方法需要將其表示的文件與編碼文件分開。所以,這種方法不能應用于所有的文件。其它的壓縮技術包括變長編碼(也被稱為哈夫曼編碼),還有其它的方法。要想了解更詳細的信息,請參考有關數據和圖像壓縮技術方面的圖書,一定會有收獲的。

數據壓縮有很多益處。不管怎么說,最主要的好處就是減少存儲方面的需求。同樣的,對于數據通信來講,壓縮數據在媒體中的將導致信息傳輸數據的提升。數據的壓縮能夠通過軟件在現有的硬件設備上實現或者通過帶有壓縮技術的特殊的硬件設備來實現。圖表2顯示了基本的數據壓縮結構圖。


圖2:數據壓縮結構圖

ZIP VS GZIP

如果你是在Windows系統下工作,你可能會對工具WinZip很熟悉,是用來創建壓縮檔案和解開壓縮檔案的。而在UNIX平臺上,會有一些不同,命令tar用來創建一個檔案文件(并不壓縮),其它的程序(gzip或compress)用來創建一個壓縮檔案。

WinZip和PkZip之類的工具同時扮演著歸檔和壓縮兩個角色。他們將文件壓縮并將其歸檔。另一方面,gzip并不將文件歸檔。所以,在UNIX平臺上,命令tar通常用來創建一個檔案文件,然后命令gzip來將檔案文件壓縮。

Java.util.zip包

Java提供了java.util.zip包用來兼容ZIP格式的數據壓縮。它提供了一系列的類用來讀取,創建,修改ZIP和GZIP格式的文件。它還提供了工具類來計算任意輸入流的數目,這可以用來驗證輸入數據的有效性。該包提供了一個接口,十四個類,和兩個異常處理類,如表1所示。

表1: java.util.zip包

條目 類型 描述
Checksum 接口 被類Adler32和CRC32實現的接口
Adler32 使用Alder32算法來計算Checksum數目
CheckedInputStream 一個輸入流,保存著被讀取數據的Checksum
CheckedOutputStream 一個輸出流,保存著被讀取數據的Checksum
CRC32 使用CRC32算法來計算Checksum數目
Deflater 使用ZLIB壓縮類,支持通常的壓縮方式
DeflaterOutputStream 一個輸出過濾流,用來壓縮Deflater格式數據
GZIPInputStream 一個輸入過濾流,讀取GZIP格式壓縮數據
GZIPOutputStream 一個輸出過濾流,讀取GZIP格式壓縮數據
Inflater 使用ZLIB壓縮類,支持通常的解壓方式
InlfaterInputStream 一個輸入過濾流,用來解壓Inlfater格式的壓縮數據
ZipEntry 存儲ZIP條目
ZipFile 從ZIP文件中讀取ZIP條目
ZipInputStream 一個輸入過濾流,用來讀取ZIP格式文件中的文件
ZipOutputStream 一個輸出過濾流,用來向ZIP格式文件口寫入文件
DataFormatException 異常類 拋出一個數據格式錯誤
ZipException 異常類 拋出一個ZIP文件


注意:ZLIB壓縮類最初是作為可移植的網絡圖像文件格式(PNG)標準的一部分開發的,是不受專利保護的。

從ZIP文件中解壓縮和提取數據

java.util.zip包提供了數據壓縮與解壓縮所需要的類。ZIP文件的解壓縮實質上就是從輸入流中讀取數據。Java.util.zip包提供了類ZipInputStream來讀取ZIP文件。ZipInputStream流的創建與其它輸入流的創建沒什么兩樣。舉個例子,下面的代碼段創建了一個輸入流來讀取ZIP格式的文件:

FileInputStream fis = new FileInputStream("figs.zip");
                                       ZipInputStream zin = new ZipInputStream(new BufferedInputStream(fis));
                                    
                                    


ZIP輸入流打開后,你可以使用getNextEntry方法來讀取ZIP文件中的條目數,該方法返回一個ZipEntry對象。如果到達文件的尾部,getNextEntry返回null:

ZipEntry entry;
                                       while((entry = zin.getNextEntry()) != null) {
                                       // extract data
                                       // open output streams
                                       }
                                    
                                    


現在,你應該建立一個輸出流,如下所示:

int BUFFER = 2048;
                                       FileOutputStream fos = new FileOutputStream(entry.getName());
                                       BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER);
                                    
                                    


注意:在這段代碼中我們用BufferedOutputStream代替了ZIPOutputStream。ZIPOutputStream和GZIPOutputStream使用內置的512字節緩沖。當緩沖區的大小大于512字節時,使用BufferedOutputStream才是正確的(例子中設置為2048)。ZIPOutputStream不允許你設置緩沖區的大小,GZIPOutputStream也是一樣,但創建 GZIPOutputStream 對象時可以通過構造函數的參數指定內置的緩沖尺寸。

這段代碼中,使用ZIP內含的條目名稱創建一個文件輸出流。可以使用entry.getName來得到它的返回句柄。接著讀出被壓縮的源數據,然后寫入輸出流:

while ((count = zin.read(data, 0, BUFFER)) != -1) {
                                       //System.out.write(x);
                                       dest.write(data, 0, count);
                                       }
                                    
                                    


最后,不要忘記關閉輸入和輸出流:

dest.flush();
                                       dest.close();
                                       zin.close();
                                    
                                    


例程1的源程序UnZip.java顯示如何解壓縮并從ZIP檔案中將文件釋放出來。測試這個例子,編譯這個類,并運行它,傳給它一個ZIP格式的文件作為參數:

prompt> java UnZip somefile.zip

注意:somefile.zip應該是一個ZIP壓縮檔案,可以用任何一種ZIP壓縮工具來創建,例如WinZip。

例程1源代碼:

UnZip.java
                                       import java.io.*;
                                       import java.util.zip.*;
                                       public class UnZip {
                                       static final int BUFFER = 2048;
                                       public static void main (String argv[]) {
                                       try {
                                       BufferedOutputStream dest = null;
                                       FileInputStream fis = new
                                       FileInputStream(argv[0]);
                                       ZipInputStream zis = new
                                       ZipInputStream(new BufferedInputStream(fis));
                                       ZipEntry entry;
                                       while((entry = zis.getNextEntry()) != null) {
                                       System.out.println("Extracting: " +entry);
                                       int count;
                                       byte data[] = new byte[BUFFER];
                                       // write the files to the disk
                                       FileOutputStream fos = new
                                       FileOutputStream(entry.getName());
                                       dest = new
                                       BufferedOutputStream(fos, BUFFER);
                                       while ((count = zis.read(data, 0, BUFFER))
                                       != -1) {
                                       dest.write(data, 0, count);
                                       }
                                       dest.flush();
                                       dest.close();
                                       }
                                       zis.close();
                                       } catch(Exception e) {
                                       e.printStackTrace();
                                       }
                                       }
                                       }
                                    
                                    


有一點值得大家注意,類ZipInputStream讀出ZIP文件序列(簡單地說就是讀出這個ZIP文件壓縮了多少文件),而類ZipFile使用內嵌的隨機文件訪問機制讀出其中的文件內容,所以不必順序的讀出ZIP壓縮文件序列。

注意:ZIPInputStream和ZipFile之間另外一個基本的不同點在于高速緩沖的使用方面。當文件使用ZipInputStream和FileInputStream流讀出的時候,ZIP條目不使用高速緩沖。然而,如果使用ZipFile(文件名)來打開文件,它將使用內嵌的高速緩沖,所以如果ZipFile(文件名)被重復調用的話,文件只被打開一次。緩沖值在第二次打開進使用。如果你工作在UNIX系統下,這是什么作用都沒有的,因為使用ZipFile打開的所有ZIP文件都在內存中存在映射,所以使用ZipFile的性能優于ZipInputStream。然而,如果同一ZIP文件的內容在程序執行期間經常改變,或是重載的話,使用ZipInputStream就成為你的首選了。

下面顯示了使用類ZipFile來解壓一個ZIP文件的過程:

  1. 通過指定一個被讀取的ZIP文件,或者是文件名,或者是一個文件對象來創建一個ZipFile對象:
    ZipFile zipfile = new ZipFile("figs.zip");

  2. 使用entries方法,返回一個枚舉對象,循環獲得文件的ZIP條目對象:
    while(e.hasMoreElements()) {
    entry = (ZipEntry) e.nextElement();
    // read contents and save them
    }

  3. ZIP條目作為參數傳遞給getInputStream方法,可以讀取ZIP文件中指定條目的內容,能過其返回的輸入流(InputStram)對象可以方便的讀出ZIP條目的內容:
    is = new BufferedInputStream(zipfile.getInputStream(entry));

  4. 獲取ZIP條目的文件名,創建輸出流,并保存:
    byte data[] = new byte[BUFFER];
    FileOutputStream fos = new FileOutputStream(entry.getName());
    dest = new BufferedOutputStream(fos, BUFFER);
    while ((count = is.read(data, 0, BUFFER)) != -1) {
    dest.write(data, 0, count);
    }

  5. 最后關閉所有的輸入輸出流 dest.flush();
    dest.close();
    is.close();

完整的程序代碼如例程2所示。再次編譯這個文件,并傳遞一個ZIP格式的文件做為參數:

prompt> java UnZip2 somefile.zip

例程2源碼:

UnZip2.java
                                       import java.io.*;
                                       import java.util.*;
                                       import java.util.zip.*;
                                       public class UnZip2 {
                                       static final int BUFFER = 2048;
                                       public static void main (String argv[]) {
                                       try {
                                       BufferedOutputStream dest = null;
                                       BufferedInputStream is = null;
                                       ZipEntry entry;
                                       ZipFile zipfile = new ZipFile(argv[0]);
                                       Enumeration e = zipfile.entries();
                                       while(e.hasMoreElements()) {
                                       entry = (ZipEntry) e.nextElement();
                                       System.out.println("Extracting: " +entry);
                                       is = new BufferedInputStream
                                       (zipfile.getInputStream(entry));
                                       int count;
                                       byte data[] = new byte[BUFFER];
                                       FileOutputStream fos = new
                                       FileOutputStream(entry.getName());
                                       dest = new
                                       BufferedOutputStream(fos, BUFFER);
                                       while ((count = is.read(data, 0, BUFFER))
                                       != -1) {
                                       dest.write(data, 0, count);
                                       }
                                       dest.flush();
                                       dest.close();
                                       is.close();
                                       }
                                       } catch(Exception e) {
                                       e.printStackTrace();
                                       }
                                       }
                                       }
                                    
                                    


將數據壓縮歸檔入一ZIP文件

類ZipOutputStream能夠用來將數據壓縮成一個ZIP文件。ZipOutputStream將數據寫入ZIP格式的輸出流。下面的步驟與創建一個ZIP文件相關。

1、第一步是創建一個ZipOutputStream對象,我們將要寫入輸出流的文件作為參數傳給它。下面的代碼演示了如何創建一個名為"myfigs.zip"的ZIP文件。
FileOutputStream dest = new
FileOutputStream("myfigs.zip");
ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(dest));

2、一但目標輸出流創建后,下一步就是打開數據源文件。在這個例子中,源數據文件是指那些當前目錄下的文件。命令list用來得到當前目錄下文件列表:

File f = new File(".");
                                       String files[] = f.list();
                                       for (int i=0; i < files.length; i++) {
                                       System.out.println("Adding: "+files[i]);
                                       FileInputStream fi = new FileInputStream(files[i]);
                                       // create zip entry
                                       // add entries to ZIP file
                                       }
                                    
                                    


注意:這個例程能夠壓縮當前目錄下的所有文件。它不能處理子目錄。作為一個練習,你可以修改例程3來處理子目錄。

3、 為讀出的數據創建一個ZIP條目列表:
ZipEntry entry = new ZipEntry(files[i]))

4、 在你將數據寫入ZIP輸出流之前,你必須使用putNextEntry方法將ZIP條目列表寫入輸出流:
out.putNextEntry(entry);

5、 將數據寫入ZIP文件:
int count;
while((count = origin.read(data, 0, BUFFER)) != -1) {
out.write(data, 0, count);
}

6、 最后關閉所有的輸入輸出流:
origin.close();
out.close();
完整的程序代碼如例程3所示。