許多信息資料都或多或少的包含一些多餘的數據。一般會致使在客戶端與服務器之間,應用程序與計算機之間極大的數據傳輸量。最多見的解決數據存儲和信息傳送的方法是安裝額外的存儲設備和擴展示有的通信能力。這樣作是能夠的,但無疑會增長組織的運做成本。一種有效的解決數據存儲與信息傳輸的方法是經過更有效率的代碼來存儲數據。這篇文章簡要的介紹了數據的壓縮與解壓縮,並展現了用java.util.zip包來實現數據的壓縮與解壓縮是多麼的方便與高效。html
固然用諸如WinZip,gzip,和Java壓縮(或jar)之類的工具也能夠實現數據的壓縮與解壓縮,這些工具都是獨立的應用程序。你也能夠在JAVA應用程序中調用這些工具,但這並非最直接的方法,也不是有效的解決方法。尤爲是你想更快速地實現數據的壓縮與解壓縮(例如在傳輸數據到遠程機器以前)。這篇文章包括如下內容:java
回頁首算法
文件中數據冗餘的最簡單的類型是"字符的複製"。讓咱們先來看下面一個字符串:服務器
JJJJJJAAAAVVVVAAAAAA 這個字符串能夠用更簡潔的方式來編碼,那就是經過替換每個重複的字符串爲單個的實例字符加上記錄重複次數的數字來表示,上面的字符串能夠被編碼爲下面的形式:網絡
6J4A4V6A 在這裏,"6J"意味着6個字符J,"4A"意味着4個字符A,以此類推。這種字符串壓縮方式稱爲"行程長度編碼"方式,簡稱RLE。socket
再舉一個例子,考慮一下矩形圖像的存儲。一個單色位圖,能夠被存儲爲下面這種形式,如圖1所示。函數
另一種方式是將圖像存爲一個圖元文件:工具
Rectangle 11, 3, 20, 5post
上面的表示方法是講矩形的起始座標是(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顯示了基本的數據壓縮結構圖。
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所示。
條目 | 類型 | 描述 |
---|---|---|
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)標準的一部分開發的,是不受專利保護的。
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。
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文件的過程:
經過指定一個被讀取的ZIP文件,或者是文件名,或者是一個文件對象來建立一個ZipFile對象: ZipFile zipfile = new ZipFile("figs.zip");
使用entries方法,返回一個枚舉對象,循環得到文件的ZIP條目對象: while(e.hasMoreElements()) { entry = (ZipEntry) e.nextElement(); // read contents and save them }
ZIP條目做爲參數傳遞給getInputStream方法,能夠讀取ZIP文件中指定條目的內容,能過其返回的輸入流(InputStram)對象能夠方便的讀出ZIP條目的內容: is = new BufferedInputStream(zipfile.getInputStream(entry));
獲取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); }
最後關閉全部的輸入輸出流 dest.flush(); dest.close(); is.close();
完整的程序代碼如清單 2所示。再次編譯這個文件,並傳遞一個ZIP格式的文件作爲參數:
prompt> java UnZip2 somefile.zip
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(); } } }
類ZipOutputStream可以用來將數據壓縮成一個ZIP文件。ZipOutputStream將數據寫入ZIP格式的輸出流。下面的步驟與建立一個ZIP文件相關。
一、 第一步是建立一個ZipOutputStream對象,咱們將要寫入輸出流的文件做爲參數傳給它。下面的代碼演示瞭如何建立一個名爲"myfigs.zip"的ZIP文件。
FileOutputStream dest = new
FileOutputStream("myfigs.zip");
ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(dest));
二、 一但目標輸出流建立後,下一步就是打開數據源文件。在這個例子中,源數據文件是指那些當前目錄下的文件。命令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來處理子目錄。
三、 爲讀出的數據建立一個ZIP條目列表:
ZipEntry entry = new ZipEntry(files[i]))
四、 在你將數據寫入ZIP輸出流以前,你必須使用putNextEntry方法將ZIP條目列表寫入輸出流:
out.putNextEntry(entry);
五、 將數據寫入ZIP文件:
int count;
while((count = origin.read(data, 0, BUFFER)) != -1) {
out.write(data, 0, count);
}
六、 最後關閉全部的輸入輸出流:
origin.close();
out.close();
完整的程序代碼如清單 3所示。
Zip.java import java.io.*; import java.util.zip.*; public class Zip { static final int BUFFER = 2048; public static void main (String argv[]) { try { BufferedInputStream origin = null; FileOutputStream dest = new FileOutputStream("c:\\zip\\myfigs.zip"); ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(dest)); //out.setMethod(ZipOutputStream.DEFLATED); byte data[] = new byte[BUFFER]; // get a list of files from current directory 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]); origin = new BufferedInputStream(fi, BUFFER); ZipEntry entry = new ZipEntry(files[i]); out.putNextEntry(entry); int count; while((count = origin.read(data, 0, BUFFER)) != -1) { out.write(data, 0, count); } origin.close(); } out.close(); } catch(Exception e) { e.printStackTrace(); } } }
注意: 條目列表能夠以兩種方式加入ZIP文件中,一種是壓縮方式(DEFLATED),另外一種是不壓縮方式(STORED),系統默認的存儲方式爲壓縮方式(DEFLATED)。SetMethod方法能夠用來設置它的存儲方式。 例如,設置存儲方式爲DEFLATED(壓縮)應該這樣作: out.setMethod(ZipOutputStream.DEFLATED) 設置存儲方式爲(不壓縮)應該這樣作: out.setMethod(ZipOutputStream.STORED)。
類ZipEntry描述了存儲在ZIP文件中的壓縮文件。類中包含有多種方法能夠用來設置和得到ZIP條目的信息。類ZipEntry是被ZipFile和ZipInputStream使用來讀取ZIP文件,ZipOutputStream來寫入ZIP文件的。ZipEntry中最有用的一些方法顯示在下面的表格2中,而且有相應的描述。
方法簽名 | 描述 |
---|---|
public String getComment() | 返回條目的註釋, 沒有返回null |
public long getCompressedSize() | 返回條目壓縮後的大小, 未知返回-1 |
public int getMethod() | 返回條目的壓縮方式,沒有指定返回 -1 |
public String getName() | 返回條目的名稱 |
public long getSize() | 返回未被壓縮的條目的大小,未知返回-1 |
public long getTime() | 返回條目的修改時間, 沒有指定返回-1 |
public void setComment(String c) | 設置條目的註釋 |
public void setMethod(int method) | 設置條目的壓縮方式 |
public void setSize(long size) | 設置沒有壓縮的條目的大小 |
public void setTime(long time) | 設置條目的修改時間 |
java.util.zip包中另一些比較重要的類是 Adler32 和 CRC32,它們實現了 java.util.zip.Checksum 接口,並估算了壓縮數據的校驗和(checksum)。衆所周知,在運算速度方面,Adler32 算法比 CRC32 算法要有必定的優點;但在數據可信度方面,CRC32算法則要更勝一籌。正所謂,"魚與熊掌,不可兼得。",你們只好在不一樣的場合下,加以取捨了。GetValue 方法能夠用來得到當前的checksum值,reset 方法可以從新設置 checksum 爲其缺省的值。
求和校驗通常用來校驗文件和信息是否正確的傳送。舉個例子,假設你想建立一個ZIP文件,而後將其傳送到遠程計算機上。當到達遠程計算機後,你就可使用checksum檢驗在傳輸過程當中文件是否發生錯誤。爲了演示如何建立checksums,咱們修改了清單 1 和清單 3,在清單 4和清單 5中使用了兩個新類,一個是CheckedInputStream,另外一個是CheckedOutputStream。(你們注意:這兩段代碼在壓縮與解壓縮過程當中,使用了同一種算法,求數據的checksum值。)
Zip.java import java.io.*; import java.util.zip.*; public class Zip { static final int BUFFER = 2048; public static void main (String argv[]) { try { BufferedInputStream origin = null; FileOutputStream dest = new FileOutputStream("c:\\zip\\myfigs.zip"); CheckedOutputStream checksum = new CheckedOutputStream(dest, new Adler32()); ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(checksum)); //out.setMethod(ZipOutputStream.DEFLATED); byte data[] = new byte[BUFFER]; // get a list of files from current directory 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]); origin = new BufferedInputStream(fi, BUFFER); ZipEntry entry = new ZipEntry(files[i]); out.putNextEntry(entry); int count; while((count = origin.read(data, 0, BUFFER)) != -1) { out.write(data, 0, count); } origin.close(); } out.close(); System.out.println("checksum: "+checksum.getChecksum().getValue()); } catch(Exception e) { e.printStackTrace(); } } }
UnZip.java import java.io.*; import java.util.zip.*; public class UnZip { public static void main (String argv[]) { try { final int BUFFER = 2048; BufferedOutputStream dest = null; FileInputStream fis = new FileInputStream(argv[0]); CheckedInputStream checksum = new CheckedInputStream(fis, new Adler32()); ZipInputStream zis = new ZipInputStream(new BufferedInputStream(checksum)); 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(); System.out.println("Checksum: "+checksum.getChecksum().getValue()); } catch(Exception e) { e.printStackTrace(); } } }
測試清單 4 和 5,編譯類文件並運行類Zip來建立一個壓縮檔案(程序會計算出checksum值並顯示在屏幕上),而後運行UnZip類來解壓縮這個檔案(屏幕上一樣會打印出一個checksum值)。兩個值必須徹底相同,不然說明出錯了。Checksums在數據校驗方面很是有用。例如,你能夠建立一個ZIP文件,而後連同checksum值一同傳遞給你的朋友。你的朋友解壓縮文件後,將生成的checksum值與你提供的做一比較,若是相同則說明在傳遞過程當中沒有發生錯誤。
咱們已經看到如何將文件中的數據壓縮並將其歸檔。但若是你想壓縮的數據不在文件中時,應該怎麼辦呢?假設有這樣一個例子,你經過套接字(socket)來傳遞一個大對象。爲了提升應用程序的性能,你可能在經過網絡開始傳遞前將數據壓縮,而後在目的地將其解壓縮。另一個例子, 咱們假設你想將一個對象用壓縮格式存儲在磁碟上,ZIP格式是基於記錄方式的,不適合這項工做。GZIP更適合用來實現這種對單一數據流的操做。 如今,咱們來示例一下,若是在寫入磁碟前將數據壓縮,並在讀出時將數據解壓縮。示清單 序6是一個在單一JVM(java虛擬機)實現了Serializable接口的簡單類,咱們想要串行化該類的實例。
Employee.java import java.io.*; public class Employee implements Serializable { String name; int age; int salary; public Employee(String name, int age, int salary) { this.name = name; this.age = age; this.salary = salary; } public void print() { System.out.println("Record for: "+name); System.out.println("Name: "+name); System.out.println("Age: "+age); System.out.println("Salary: "+salary); } }
如今,寫另一個類來建立兩個從Employee類實例化而來的對象。清單 7 從Employee類建立了兩個對象(sarah和sam)。而後將它們的狀態以壓縮的格式存儲在一個文件中。
SaveEmployee.java import java.io.*; import java.util.zip.*; public class SaveEmployee { public static void main(String argv[]) throws Exception { // create some objects Employee sarah = new Employee("S. Jordan", 28, 56000); Employee sam = new Employee("S. McDonald", 29, 58000); // serialize the objects sarah and sam FileOutputStream fos = new FileOutputStream("db"); GZIPOutputStream gz = new GZIPOutputStream(fos); ObjectOutputStream oos = new ObjectOutputStream(gz); oos.writeObject(sarah); oos.writeObject(sam); oos.flush(); oos.close(); fos.close(); } }
如今,清單 8 中的 ReadEmpolyee 類是用來從新構建兩個對象的狀態。一但構建成功,就調用print方法將其打印出來。
ReadEmployee.java import java.io.*; import java.util.zip.*; public class ReadEmployee { public static void main(String argv[]) throws Exception{ //deserialize objects sarah and sam FileInputStream fis = new FileInputStream("db"); GZIPInputStream gs = new GZIPInputStream(fis); ObjectInputStream ois = new ObjectInputStream(gs); Employee sarah = (Employee) ois.readObject(); Employee sam = (Employee) ois.readObject(); //print the records after reconstruction of state sarah.print(); sam.print(); ois.close(); fis.close(); } }
一樣的思想能夠用於在網絡間經過(socket)傳輸的大對象。下面的代碼段示例瞭如何在客戶/服務器之間實現大對象的壓縮:
// write to client GZIPOutputStream gzipout = new GZIPOutputStream(socket.getOutputStream()); ObjectOutputStream oos = new ObjectOutputStream(gzipout); oos.writeObject(obj); gzipos.finish();
下面的代碼段顯示了客戶端從服務器端接收到數據後,如何將其解壓:
// read from server Socket socket = new Socket(remoteServerIP, PORT); GZIPInputStream gzipin = new GZIPInputStream(socket.getInputStream()); ObjectInputStream ois = new ObjectInputStream(gzipin); Object o = ois.readObject();
Java檔案文件(JAR)格式是基於標準的ZIP文件格式,並附有可選擇的文件清單列表。若是你想要在你個人應用程序中建立JAR文件或從JAR文件中解壓縮文件,可使用java.util.jar包,它提供了讀寫JAR文件的類。使用java.util.jar包提供的類與本文所講述的java.util.zip包十分類似。因此你應該可以從新編寫本文的源代碼,若是你想使用java.util.jar包的話。
本文討論了你能夠在應用程序中使用的數據壓縮與解壓的應用程序接口,本文的示例程序演示瞭如何使用java.util.zip包來壓縮數據與解壓縮數據。如今你能夠利用這個工具在你的應用程序中實現數據的壓縮與解壓了。
本文也說明了如何在絡傳輸中實現數據的壓縮與解壓縮,以減小網絡阻塞和加強你的客戶/服務器模式應用程序的性能。在網絡傳輸中實現數據的壓縮,只有當傳輸的數據量達到成百上千字節時,你纔會感受到程序性能的提高,若是僅僅是傳遞一個字符串對象,對應用程序是沒什麼影響的。