ユーザがアプリの保存データを扱う場面を思い浮かべると、できるだけ容量が小さく、できれば1つのファイルにまとまっている方が持ち運びなどに便利だろう。そう考えると、保存データを圧縮するという方法が有力な選択肢の1つとして考えられる。
というわけで、今回はJavaの標準ライブラリ(java.util.zip)を利用した圧縮・解凍方法を確認する。Javaの標準ライブラリではzip圧縮とgzip圧縮が利用可能となっている。それ以外の圧縮形式(7z、tar.gz(gzip+tar)など)に関しては外部ライブラリを使うと利用できるようになるが、こちらは別の記事で確認する。
■ Java標準ライブラリで利用可能な圧縮方式
Javaにおいて圧縮・解凍を扱うクラスはjava.util.zipパッケージにまとめられている(
*1)。java.util.zipパッケージで利用可能な圧縮方式は以下のとおりである。
圧縮形式 |
関連クラス |
内容 |
DEFLATE / INFLATE |
Deflater
DeflaterInputStream
DeflaterOutputStream
Inflater
InflaterInputStream
InflaterOutputStream |
インターネットで広く使われている圧縮アルゴリズム。zipやgzip内で利用されている。 |
GZIP |
GZIPInputStream
GZIPOutputStream |
単一ファイルの圧縮・解凍を行う圧縮形式。複数ファイルを扱う際は、tar圧縮がよく併用される(「*.tar.gz」ファイル)。GZIPはGNU zipの略 |
ZIP |
ZipEntry
ZipFile
ZipInputStream
ZipOutputStream |
Windowsで一般的な圧縮形式。複数ファイルの圧縮・解凍が可能 |
また、java.util.zipパッケージには上記のほかにチェックサムを扱うAdler32クラスやCRC32クラスが存在し、チェックサムを利用して入出力するためのクラス(CheckedInputStream、ChedkedOutputStream)が存在する。
■ サンプル・プログラム1(zip圧縮・解凍)
以下にJava標準ライブラリでzip圧縮・解凍するサンプルプログラムを示す。サンプルではフォルダを含む3つのファイルをzip圧縮・解凍している。
◇リソース
TestJava(プロジェクトフォルダ)
┣ src
┃ ┗ application
┃ ┗ TestCompress1.java
┣ input
┃ ┣ file1.txt
┃ ┣ file2.txt
┃ ┣ sub1
┃ ┃ ┗ file3.txt
┃ ┗ sub2
┗ output
◇サンプルコード
package application;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
/**
* 圧縮・解凍の利用
* @author karura
*
*/
public class TestCompress1
{
/**
* 起動用関数
* @param args
* @throws Exception
*/
public static void main( String[] args ) throws Exception
{
new TestCompress1();
}
/**
* 処理メイン
* @throws Exception
*/
public TestCompress1() throws Exception
{
// 開始メッセージ
System.out.println( "start!" );
// ファイルの圧縮
String baseDir = "input";
String[] files = { "file1.txt" , "file2.txt" , "sub1/file3.txt" , "sub2/"};
// zip圧縮
compressZip( baseDir , files , "output/zipfile.zip" );
// zip解凍
decompressZip( "output/zipfile.zip" , "output/zipfile/" );
// 終了メッセージ
System.out.println( "end!" );
}
/**
* ファイルをzip圧縮する
* @param inputBaseDir 入力ファイルを指定する際のベース・フォルダ・パス
* @param inputFiles 圧縮するファイル名。inputBaseDirからの相対パスで指定
* @param outputFile 作成するzipファイル名
* @throws Exception
*/
protected void compressZip( String inputBaseDir , String[] inputFiles , String outputFile ) throws Exception
{
// nullチェック
if( inputBaseDir == null ){ return; }
if( inputFiles == null ){ return; }
if( outputFile == null ){ return; }
// zipファイルの作成
// try-with-resource構文でファイルcloseしている
try(
// 出力ファイル指定
FileOutputStream out = new FileOutputStream( outputFile );
BufferedOutputStream bos = new BufferedOutputStream( out );
ZipOutputStream archive = new ZipOutputStream( out ) )
{
// 入力ファイルの数だけエントリーを追加
for( String fileName : inputFiles )
{
// エントリー1つ分を出力開始
ZipEntry entry = new ZipEntry( fileName );
archive.putNextEntry( entry );
// フォルダの場合は次へ
if( fileName.endsWith("/") )
{
archive.closeEntry();
continue;
}
// 入力ファイル指定
try( FileInputStream fis = new FileInputStream( inputBaseDir + "/" + fileName );
BufferedInputStream bis = new BufferedInputStream( fis ))
{
// エントリーの中身を出力
int size = 0;
byte[] buf = new byte[ 1024 ];
while( ( size = bis.read( buf ) ) > 0 )
{
archive.write( buf , 0 , size );
}
}
// エントリー1つ文の出力を終了
archive.closeEntry();
}
}
}
/**
* zip解凍
* @param inputFile 解凍するzipファイル
* @param outputDir 解凍先フォルダ
* @throws Exception
*/
protected void decompressZip( String inputFile , String outputDir ) throws Exception
{
// zipファイルの読込
// try-with-resource構文でファイルcloseしている
try( FileInputStream fis = new FileInputStream( inputFile );
ZipInputStream archive = new ZipInputStream( fis ) )
{
// エントリーを1つずつファイル・フォルダに復元
ZipEntry entry = null;
while( ( entry = archive.getNextEntry() ) != null )
{
// ファイルを作成
File file = new File( outputDir + "/" + entry.getName() );
// フォルダ・エントリの場合はフォルダを作成して次へ
if( entry.isDirectory() )
{
file.mkdirs();
continue;
}
// ファイル出力する場合、
// フォルダが存在しない場合は事前にフォルダ作成
if( !file.getParentFile().exists() ){ file.getParentFile().mkdirs(); }
// ファイル出力
try( FileOutputStream fos = new FileOutputStream( file ) ;
BufferedOutputStream bos = new BufferedOutputStream( fos ) )
{
// エントリーの中身を出力
int size = 0;
byte[] buf = new byte[ 1024 ];
while( ( size = archive.read( buf ) ) > 0 )
{
bos.write( buf , 0 , size );
}
}
}
}
}
}
◇実行結果
┗ output
┣ zipfile.zip
┗ zipfile
┣ file1.txt
┣ file2.txt
┣ sub1
┃ ┗ file3.txt
┗ sub2
◇解説
zip圧縮はcompressZip関数(45行目)、zip解凍はdecompressZip関数(48行目)で実施している。compressZip関数の内部(59行目~105行目)では、ZipOutputStreamクラス(72行目)に対してエントリ(Zipファイル内のファイルのようなもの)を1つ1つ登録している(75行目~104行目)。エントリーの登録では、ヘッダ情報をZipEntryクラスとして登録(78行目~79行目)してから、ファイルの内容を登録している(89行目~99行目)。
解凍は圧縮の逆で、zipファイル内のエントリーを取得(122行目)してから、ファイルを1つ1つ作成している(122行目~150行目)。
■ サンプル・プログラム2(ソケット通信の圧縮)
以下にソケット通信を圧縮するサンプルプログラムを示す。サンプルでは圧縮した文字列を送るサーバ(TestCompress2Server)と受信・表示するクライアント(TestCompress2Client)の2つのプログラムが、localhost上のポートを経由して文字列を送受信する。
◇サンプルコード
package application;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.zip.GZIPOutputStream;
/**
* 圧縮通信のサーバ
* @author karura
*
*/
public class TestCompress2Server
{
/**
* 起動用関数
* @param args
* @throws Exception
*/
public static void main( String[] args ) throws Exception
{
new TestCompress2Server();
}
/**
* 処理メイン
* @throws Exception
*/
public TestCompress2Server() throws Exception
{
// 開始メッセージ
final int PORT = 10000;
System.out.println( "start");
// ポート番号を指定して、ソケットを関連付ける
// try-with-resource構文でcloseしている
try( ServerSocket serverSocket = new ServerSocket( PORT ) )
{
// ループ
while( true )
{
// 接続を待ち受け
System.out.println( "wait for " + PORT );
Socket socket = serverSocket.accept();
// 接続メッセージ
System.out.println( "接続!" );
// 圧縮通信をInputStreamとして開く
// try-with-resource構文でcloseしている
String str = "Hello. 1234567890";
GZIPOutputStream out = new GZIPOutputStream( socket.getOutputStream() );
// メッセージを送信
byte[] buf = str.getBytes();
out.write( buf , 0 , str.length() );
// 更新
out.finish();
out.close();
// ソケットを閉じる
socket.close();
// 終了メッセージ
System.out.println( "end!" );
}
}
}
}
package application;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.zip.GZIPOutputStream;
/**
* 圧縮通信のサーバ
* @author karura
*
*/
public class TestCompress2Server
{
/**
* 起動用関数
* @param args
* @throws Exception
*/
public static void main( String[] args ) throws Exception
{
new TestCompress2Server();
}
/**
* 処理メイン
* @throws Exception
*/
public TestCompress2Server() throws Exception
{
// 開始メッセージ
final int PORT = 10000;
System.out.println( "start");
// ポート番号を指定して、ソケットを関連付ける
// try-with-resource構文でcloseしている
try( ServerSocket serverSocket = new ServerSocket( PORT ) )
{
// ループ
while( true )
{
// 接続を待ち受け
System.out.println( "wait for " + PORT );
Socket socket = serverSocket.accept();
// 接続メッセージ
System.out.println( "接続!" );
// 圧縮通信をInputStreamとして開く
// try-with-resource構文でcloseしている
String str = "Hello. 1234567890";
GZIPOutputStream out = new GZIPOutputStream( socket.getOutputStream() );
// メッセージを送信
byte[] buf = str.getBytes();
out.write( buf , 0 , str.length() );
// 更新
out.finish();
out.close();
// ソケットを閉じる
socket.close();
// 終了メッセージ
System.out.println( "end!" );
}
}
}
}
package application;
import java.net.Socket;
import java.util.zip.GZIPInputStream;
/**
* 圧縮通信のクライアント
* @author karura
*
*/
public class TestCompress2Client
{
/**
* 起動用関数
* @param args
* @throws Exception
*/
public static void main( String[] args ) throws Exception
{
new TestCompress2Client();
}
/**
* 処理メイン
* @throws Exception
*/
public TestCompress2Client() throws Exception
{
// 開始メッセージ
final int PORT = 10000;
System.out.println( "start!" );
// ソケットを作成
try( Socket socket = new Socket( "localhost" , PORT ) )
{
// サーバに文字を受信
System.out.println( "接続!" );
// 圧縮通信をInputStreamとして開く
// try-with-resource構文でcloseしている
try( GZIPInputStream in = new GZIPInputStream( socket.getInputStream() ) )
{
// 圧縮情報を受け取り、解凍・出力
int size = 0;
byte[] buf = new byte[ 1024 ];
while( ( size = in.read( buf ) ) > 0 )
{
// 解凍した情報を出力
System.out.println( size );
System.out.println( new String(buf,0,size) );
}
}
}
// 終了メッセージ
System.out.println( "end!" );
}
}
◇実行結果
start
wait for 10000
接続!
end!
wait for 10000
start!
接続!
17
Hello. 1234567890
end!
◇解説
まず、出力結果の見方についての注意点を説明する。Eclipse上で2つのプログラムを起動すると以下の画面のように標準出力を表示するコンソールが片方のプログラム分しか見えなくなる。表示するコンソールを変更するには「選択されたコンソールの表示」ボタンを押下して、見たいコンソールを選択する必要がある。
図:「選択されたコンソールの表示」ボタンの位置
次にプログラムを見ていく。GZIP圧縮では内部にエントリを持っていないので圧縮・解凍は非常に簡単で、GZIPOutputStream/GZIPInputStreamに対してwrite/read関数でデータを読書すればよいだけである(TestCompress2Server.java 52行目~63行目、TestCompress2Client 41行目~52行目)。なお、実際に利用するには適切な例外処理が必要となる(out.closeあたり)。
ソケット通信のデータを圧縮する際の注意点としては、内部にエントリ情報(複数のファイル)を持っていないGZIPやDEFLATE形式で圧縮・解凍を利用する必要がある点があげられる(
*2)。
■ 参照
- JavaDoc 「パッケージjava.util.zipの階層」
- stack overflow 「Problems with using ZipOutputStream and ObjectOutputStream」
- java2s.com 「Compressed socket : ServerSocket « Network Protocol « Java」
1. 無題
圧縮の仕方、勉強になります。