java-recipes

ホーム エンコーディング・圧縮 › E-02

E-02: ZIP/GZIP 圧縮・解凍

java.util.zip ZipOutputStreamGZIPOutputStream で複数ファイルの ZIP 圧縮と単一ファイルの GZIP 圧縮を解説します。 圧縮レベルのトレードオフと、よくある落とし穴も紹介します。

いつ使うか

  • 複数のファイルをまとめて1つのアーカイブに圧縮して配布・転送するとき(ZIP)
  • ログファイルやテキストデータをサーバー間で転送する際にサイズを削減するとき(GZIP)
  • Web サーバーのレスポンスを GZIP 圧縮してネットワーク転送量を削減するとき
  • バックアップファイルを圧縮して保存容量を節約するとき

ZIP と GZIP の比較

形式ファイル数用途クラス
ZIP複数可ファイル配布・アーカイブZipOutputStream
GZIP1つのみ転送・ログ圧縮GZIPOutputStream

サンプルコード

ZipGzipSample.java
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.zip.*;

public class ZipGzipSample {

    // 複数エントリを1つの ZIP に圧縮
    public static byte[] createZip(String[] fileNames, String[] contents) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (ZipOutputStream zos = new ZipOutputStream(baos)) {
            for (int i = 0; i < fileNames.length; i++) {
                ZipEntry entry = new ZipEntry(fileNames[i]);
                zos.putNextEntry(entry);
                zos.write(contents[i].getBytes(StandardCharsets.UTF_8));
                zos.closeEntry(); // 忘れると ZIP が壊れるので必須
            }
        }
        return baos.toByteArray();
    }

    // ZIP を展開してエントリ一覧と内容を取得
    public static void readZip(byte[] zipData) throws IOException {
        try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipData))) {
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                ByteArrayOutputStream content = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024];
                int len;
                while ((len = zis.read(buffer)) != -1) {
                    content.write(buffer, 0, len);
                }
                System.out.println("ファイル: " + entry.getName()
                        + " (圧縮後: " + entry.getCompressedSize() + " bytes)");
                System.out.println("内容: " + content.toString("UTF-8"));
                zis.closeEntry();
            }
        }
    }

    // 単一データを GZIP 圧縮
    public static byte[] gzipCompress(String text) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (GZIPOutputStream gos = new GZIPOutputStream(baos)) {
            gos.write(text.getBytes(StandardCharsets.UTF_8));
        }
        return baos.toByteArray();
    }

    // GZIP 解凍
    public static String gzipDecompress(byte[] compressed) throws IOException {
        ByteArrayInputStream bais = new ByteArrayInputStream(compressed);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (GZIPInputStream gis = new GZIPInputStream(bais)) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = gis.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
        }
        return baos.toString("UTF-8");
    }

    public static void main(String[] args) throws Exception {
        // ZIP デモ
        System.out.println("=== ZIP 圧縮・解凍 ===");
        String[] names = {"hello.txt", "data.csv"};
        String[] contents = {"Hello, World!\n日本語テスト", "id,name\n1,田中\n2,鈴木"};
        byte[] zip = createZip(names, contents);
        System.out.println("ZIP サイズ: " + zip.length + " bytes");
        readZip(zip);

        // GZIP デモ
        System.out.println("\n=== GZIP 圧縮・解凍 ===");
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10; i++) {
            sb.append("GZIP 圧縮テスト。同じ文字列が繰り返されると圧縮率が高くなります。");
        }
        String text = sb.toString();
        byte[] compressed = gzipCompress(text);
        int originalSize = text.getBytes(StandardCharsets.UTF_8).length;
        System.out.printf("元サイズ: %d bytes → 圧縮後: %d bytes (%.1f%%圧縮)%n",
                originalSize, compressed.length,
                (1.0 - (double) compressed.length / originalSize) * 100);
        String decompressed = gzipDecompress(compressed);
        System.out.println("解凍後一致: " + text.equals(decompressed));
    }
}

ZipOutputStream を閉じると自動的に ZIP の終端マーカーが書き込まれます。try-with-resources を使えばクローズ忘れを防げます。

よくあるミス・注意点

⚠️ closeEntry() を忘れると ZIP が壊れる

ZipOutputStream では各エントリのデータを書き込んだ後に必ず closeEntry() を呼んでください。忘れると次のエントリの開始位置が正しく書き込まれず、展開時にエラーになります。

⚠️ ZIP 内にディレクトリ構造を作るにはパスを含めればよい

new ZipEntry("dir/file.txt") のようにスラッシュを含むパスを指定するだけで、ZIP 内にディレクトリ構造を作れます。 ディレクトリ自体は new ZipEntry("dir/") と末尾スラッシュで作成します。

⚠️ GZIP は1ファイルしか圧縮できない

GZIP フォーマットは単一のデータストリームを圧縮するものです。複数ファイルをまとめて GZIP 圧縮したい場合は、 まず tar でまとめて(tar.gz)か ZIP を使ってください。

⚠️ 圧縮レベルは速度と圧縮率のトレードオフ

Deflater.BEST_SPEED(level=1)は圧縮率より速度を優先し、Deflater.BEST_COMPRESSION(level=9)は最大圧縮率を優先します。 リアルタイム圧縮には BEST_SPEED、バックアップには BEST_COMPRESSION が向いています。 デフォルトは level=6 でバランス型です。

テストする観点

  • ZIP 圧縮 → 解凍で元のファイル名と内容が一致すること(往復変換)
  • GZIP 圧縮 → 解凍で元の文字列と一致すること(往復変換)
  • 空のコンテンツ(空文字列)をエントリとして追加しても正常に圧縮・解凍できること(境界値)
  • 日本語ファイル名のエントリが正しく圧縮・解凍できること
  • バイナリデータ(バイト配列)が正しく圧縮・解凍できること
  • 圧縮後のサイズが元のサイズより小さくなること(繰り返しデータで確認)

GitHub でソースコードを見る →