JAVA中的壓縮與解壓縮
2007-10-13 00:58
本文通過(guò)對(duì)數(shù)據(jù)壓縮算法的簡(jiǎn)要介紹,然后以詳細(xì)的示例演示了利用java.util.zip包實(shí)現(xiàn)數(shù)據(jù)的壓縮與解壓,并擴(kuò)展到在網(wǎng)絡(luò)傳輸方面如何應(yīng)用java.util.zip包現(xiàn)數(shù)據(jù)壓縮與解壓

綜述

許多信息資料都或多或少的包含一些多余的數(shù)據(jù)。通常會(huì)導(dǎo)致在客戶(hù)端與服務(wù)器之間,應(yīng)用程序與計(jì)算機(jī)之間極大的數(shù)據(jù)傳輸量。最常見(jiàn)的解決數(shù)據(jù)存儲(chǔ)和信息傳送的方法是安裝額外的存儲(chǔ)設(shè)備和擴(kuò)展現(xiàn)有的通訊能力。這樣做是可以的,但無(wú)疑會(huì)增加組織的運(yùn)作成本。一種有效的解決數(shù)據(jù)存儲(chǔ)與信息傳輸?shù)姆椒ㄊ峭ㄟ^(guò)更有效率的代碼來(lái)存儲(chǔ)數(shù)據(jù)。這篇文章簡(jiǎn)要的介紹了數(shù)據(jù)的壓縮與解壓縮,并展示了用java.util.zip包來(lái)實(shí)現(xiàn)數(shù)據(jù)的壓縮與解壓縮是多么的方便與高效。

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

  • 給出一個(gè)關(guān)于數(shù)據(jù)壓縮的簡(jiǎn)單的介紹
  • 描述java.util.zip包
  • 示例如何使用該包實(shí)現(xiàn)數(shù)據(jù)的壓縮與解壓縮
  • 示例如何壓縮串行化的對(duì)象并將其存儲(chǔ)在磁碟上
  • 示例如何通過(guò)數(shù)據(jù)壓縮來(lái)增強(qiáng)"客戶(hù)/服務(wù)"應(yīng)用程序的性能

數(shù)據(jù)壓縮概述

文件中數(shù)據(jù)冗余的最簡(jiǎn)單的類(lèi)型是"字符的復(fù)制"。讓我們先來(lái)看下面一個(gè)字符串:

  • JJJJJJAAAAVVVVAAAAAA
    這個(gè)字符串可以用更簡(jiǎn)潔的方式來(lái)編碼,那就是通過(guò)替換每一個(gè)重復(fù)的字符串為單個(gè)的實(shí)例字符加上記錄重復(fù)次數(shù)的數(shù)字來(lái)表示,上面的字符串可以被編碼為下面的形式:

  • ***A4V6A
    在這里,"6J"意味著6個(gè)字符J,"4A"意味著4個(gè)字符A,以此類(lèi)推。這種字符串壓縮方式稱(chēng)為"行程長(zhǎng)度編碼"方式,簡(jiǎn)稱(chēng)RLE。

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


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

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

Rectangle 11, 3, 20, 5

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

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

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個(gè)0組成。第三行是講圖像的第三行是由10個(gè)0加上20個(gè)1再加上10個(gè)0組成,其它行以此類(lèi)推。

大家注意,RLE方法需要將其表示的文件與編碼文件分開(kāi)。所以,這種方法不能應(yīng)用于所有的文件。其它的壓縮技術(shù)包括變長(zhǎng)編碼(也被稱(chēng)為哈夫曼編碼),還有其它的方法。要想了解更詳細(xì)的信息,請(qǐng)參考有關(guān)數(shù)據(jù)和圖像壓縮技術(shù)方面的圖書(shū),一定會(huì)有收獲的。

數(shù)據(jù)壓縮有很多益處。不管怎么說(shuō),最主要的好處就是減少存儲(chǔ)方面的需求。同樣的,對(duì)于數(shù)據(jù)通信來(lái)講,壓縮數(shù)據(jù)在媒體中的將導(dǎo)致信息傳輸數(shù)據(jù)的提升。數(shù)據(jù)的壓縮能夠通過(guò)軟件在現(xiàn)有的硬件設(shè)備上實(shí)現(xiàn)或者通過(guò)帶有壓縮技術(shù)的特殊的硬件設(shè)備來(lái)實(shí)現(xiàn)。圖表2顯示了基本的數(shù)據(jù)壓縮結(jié)構(gòu)圖。


圖2:數(shù)據(jù)壓縮結(jié)構(gòu)圖

ZIP VS GZIP

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

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

Java.util.zip包

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

表1: java.util.zip包

條目 類(lèi)型 描述
Checksum 接口 被類(lèi)Adler32和CRC32實(shí)現(xiàn)的接口
Adler32 類(lèi) 使用Alder32算法來(lái)計(jì)算Checksum數(shù)目
CheckedInputStream 類(lèi) 一個(gè)輸入流,保存著被讀取數(shù)據(jù)的Checksum
CheckedOutputStream 類(lèi) 一個(gè)輸出流,保存著被讀取數(shù)據(jù)的Checksum
CRC32 類(lèi) 使用CRC32算法來(lái)計(jì)算Checksum數(shù)目
Deflater 類(lèi) 使用ZLIB壓縮類(lèi),支持通常的壓縮方式
DeflaterOutputStream 類(lèi) 一個(gè)輸出過(guò)濾流,用來(lái)壓縮Deflater格式數(shù)據(jù)
GZIPInputStream 類(lèi) 一個(gè)輸入過(guò)濾流,讀取GZIP格式壓縮數(shù)據(jù)
GZIPOutputStream 類(lèi) 一個(gè)輸出過(guò)濾流,讀取GZIP格式壓縮數(shù)據(jù)
Inflater 類(lèi) 使用ZLIB壓縮類(lèi),支持通常的解壓方式
InlfaterInputStream 類(lèi) 一個(gè)輸入過(guò)濾流,用來(lái)解壓Inlfater格式的壓縮數(shù)據(jù)
ZipEntry 類(lèi) 存儲(chǔ)ZIP條目
ZipFile 類(lèi) 從ZIP文件中讀取ZIP條目
ZipInputStream 類(lèi) 一個(gè)輸入過(guò)濾流,用來(lái)讀取ZIP格式文件中的文件
ZipOutputStream 類(lèi) 一個(gè)輸出過(guò)濾流,用來(lái)向ZIP格式文件口寫(xiě)入文件
DataFormatException 異常類(lèi) 拋出一個(gè)數(shù)據(jù)格式錯(cuò)誤
ZipException 異常類(lèi) 拋出一個(gè)ZIP文件


注意:ZLIB壓縮類(lèi)最初是作為可移植的網(wǎng)絡(luò)圖像文件格式(PNG)標(biāo)準(zhǔn)的一部分開(kāi)發(fā)的,是不受專(zhuān)利保護(hù)的。

從ZIP文件中解壓縮和提取數(shù)據(jù)

java.util.zip包提供了數(shù)據(jù)壓縮與解壓縮所需要的類(lèi)。ZIP文件的解壓縮實(shí)質(zhì)上就是從輸入流中讀取數(shù)據(jù)。Java.util.zip包提供了類(lèi)ZipInputStream來(lái)讀取ZIP文件。ZipInputStream流的創(chuàng)建與其它輸入流的創(chuàng)建沒(méi)什么兩樣。舉個(gè)例子,下面的代碼段創(chuàng)建了一個(gè)輸入流來(lái)讀取ZIP格式的文件:

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


ZIP輸入流打開(kāi)后,你可以使用getNextEntry方法來(lái)讀取ZIP文件中的條目數(shù),該方法返回一個(gè)ZipEntry對(duì)象。如果到達(dá)文件的尾部,getNextEntry返回null:

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


現(xiàn)在,你應(yīng)該建立一個(gè)輸出流,如下所示:

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


注意:在這段代碼中我們用BufferedOutputStream代替了ZIPOutputStream。ZIPOutputStream和GZIPOutputStream使用內(nèi)置的512字節(jié)緩沖。當(dāng)緩沖區(qū)的大小大于512字節(jié)時(shí),使用BufferedOutputStream才是正確的(例子中設(shè)置為2048)。ZIPOutputStream不允許你設(shè)置緩沖區(qū)的大小,GZIPOutputStream也是一樣,但創(chuàng)建 GZIPOutputStream 對(duì)象時(shí)可以通過(guò)構(gòu)造函數(shù)的參數(shù)指定內(nèi)置的緩沖尺寸。

這段代碼中,使用ZIP內(nèi)含的條目名稱(chēng)創(chuàng)建一個(gè)文件輸出流。可以使用entry.getName來(lái)得到它的返回句柄。接著讀出被壓縮的源數(shù)據(jù),然后寫(xiě)入輸出流:

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


最后,不要忘記關(guān)閉輸入和輸出流:

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


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

prompt> java UnZip somefile.zip

注意:somefile.zip應(yīng)該是一個(gè)ZIP壓縮檔案,可以用任何一種ZIP壓縮工具來(lái)創(chuàng)建,例如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();
                                       }
                                       }
                                       }
                                    
                                    


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

注意:ZIPInputStream和ZipFile之間另外一個(gè)基本的不同點(diǎn)在于高速緩沖的使用方面。當(dāng)文件使用ZipInputStream和FileInputStream流讀出的時(shí)候,ZIP條目不使用高速緩沖。然而,如果使用ZipFile(文件名)來(lái)打開(kāi)文件,它將使用內(nèi)嵌的高速緩沖,所以如果ZipFile(文件名)被重復(fù)調(diào)用的話(huà),文件只被打開(kāi)一次。緩沖值在第二次打開(kāi)進(jìn)使用。如果你工作在UNIX系統(tǒng)下,這是什么作用都沒(méi)有的,因?yàn)槭褂肸ipFile打開(kāi)的所有ZIP文件都在內(nèi)存中存在映射,所以使用ZipFile的性能優(yōu)于ZipInputStream。然而,如果同一ZIP文件的內(nèi)容在程序執(zhí)行期間經(jīng)常改變,或是重載的話(huà),使用ZipInputStream就成為你的首選了。

下面顯示了使用類(lèi)ZipFile來(lái)解壓一個(gè)ZIP文件的過(guò)程:

  1. 通過(guò)指定一個(gè)被讀取的ZIP文件,或者是文件名,或者是一個(gè)文件對(duì)象來(lái)創(chuàng)建一個(gè)ZipFile對(duì)象:
    ZipFile zipfile = new ZipFile("figs.zip");

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

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

  4. 獲取ZIP條目的文件名,創(chuàng)建輸出流,并保存:
    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. 最后關(guān)閉所有的輸入輸出流 dest.flush();
    dest.close();
    is.close();

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

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();
                                       }
                                       }
                                       }
                                    
                                    


將數(shù)據(jù)壓縮歸檔入一ZIP文件

類(lèi)ZipOutputStream能夠用來(lái)將數(shù)據(jù)壓縮成一個(gè)ZIP文件。ZipOutputStream將數(shù)據(jù)寫(xiě)入ZIP格式的輸出流。下面的步驟與創(chuàng)建一個(gè)ZIP文件相關(guān)。

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

2、一但目標(biāo)輸出流創(chuàng)建后,下一步就是打開(kāi)數(shù)據(jù)源文件。在這個(gè)例子中,源數(shù)據(jù)文件是指那些當(dāng)前目錄下的文件。命令list用來(lái)得到當(dāng)前目錄下文件列表:

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
                                       }
                                    
                                    


注意:這個(gè)例程能夠壓縮當(dāng)前目錄下的所有文件。它不能處理子目錄。作為一個(gè)練習(xí),你可以修改例程3來(lái)處理子目錄。

3、 為讀出的數(shù)據(jù)創(chuàng)建一個(gè)ZIP條目列表:
ZipEntry entry = new ZipEntry(files[i]))

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

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

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