概要

ファイルの圧縮と解凍は、帳票のまとめ送信・ログファイルのアーカイブ・API レスポンスの GZIP 圧縮など、業務システムで日常的に必要になる処理です。Java には java.util.zip パッケージが標準で用意されており、外部ライブラリなしで ZIP と GZIP の両方を扱えます。ただし、ZipEntry の closeEntry 忘れ、ストリームのクローズ順序、文字コード指定の漏れなど、動作はするが不具合の温床になるコードを書きやすい領域でもあります。この記事では、複数ファイルの ZIP 圧縮・展開と GZIP の単一データ圧縮・解凍を、try-with-resources によるリソース管理を含めた安全なパターンで整理します。圧縮率の目安やバッファサイズの選び方など、実運用で気になるポイントも補足します。

使いどころ

月次バッチで生成した複数の帳票ファイル(PDF・CSV)を1つの ZIP にまとめ、メール添付用に出力する

サーバー間のログ転送で GZIP 圧縮をかけ、転送量を削減する

外部システムから受信した ZIP ファイルを展開し、中のCSVを1件ずつ取り込みバッチ処理する

コード例

ZipGzipExample.java
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.*;

public class ZipGzipExample {

    record ZipEntryData(String name, String content) {}

    /** 複数エントリを1つの ZIP に圧縮 */
    public static byte[] createZip(List<ZipEntryData> entries) throws IOException {
        var baos = new ByteArrayOutputStream();
        try (var zos = new ZipOutputStream(baos)) {
            for (var entry : entries) {
                zos.putNextEntry(new ZipEntry(entry.name()));
                zos.write(entry.content().getBytes(StandardCharsets.UTF_8));
                zos.closeEntry();
            }
        }
        return baos.toByteArray();
    }

    /** ZIP を展開して List<ZipEntryData> として返す */
    public static List<ZipEntryData> readZip(byte[] zipData) throws IOException {
        var result = new ArrayList<ZipEntryData>();
        try (var zis = new ZipInputStream(new ByteArrayInputStream(zipData))) {
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                var content = new ByteArrayOutputStream();
                var buffer = new byte[1024];
                int len;
                while ((len = zis.read(buffer)) != -1) {
                    content.write(buffer, 0, len);
                }
                result.add(new ZipEntryData(entry.getName(),
                        content.toString(StandardCharsets.UTF_8)));
                zis.closeEntry();
            }
        }
        return result;
    }

    /** 文字列を GZIP 圧縮 */
    public static byte[] gzipCompress(String text) throws IOException {
        var baos = new ByteArrayOutputStream();
        try (var gos = new GZIPOutputStream(baos)) {
            gos.write(text.getBytes(StandardCharsets.UTF_8));
        }
        return baos.toByteArray();
    }

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

    public static void main(String[] args) throws Exception {
        // ZIP 圧縮・解凍
        var entries = List.of(
                new ZipEntryData("report.csv", "id,name\n1,田中\n2,鈴木"),
                new ZipEntryData("memo.txt", "処理完了")
        );
        var zip = createZip(entries);
        System.out.println("ZIP サイズ: " + zip.length + " bytes");

        var extracted = readZip(zip);
        extracted.forEach(e ->
                System.out.println(e.name() + ": " + e.content()));

        // GZIP 圧縮・解凍
        var text = "GZIP 圧縮テスト".repeat(50);
        var compressed = gzipCompress(text);
        var original = text.getBytes(StandardCharsets.UTF_8).length;
        System.out.printf("GZIP: %d → %d bytes (%.0f%%削減)%n",
                original, compressed.length,
                (1.0 - (double) compressed.length / original) * 100);
        System.out.println("解凍一致: " + text.equals(gzipDecompress(compressed)));
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

var と record を使って ZIP エントリのデータを構造化できる。List.of によるエントリリストの作成も簡潔になる。String.repeat で圧縮テスト用データの生成が楽。

Java 17
// Java 17: record + List.of でエントリを構造化
record ZipEntryData(String name, String content) {}
var entries = List.of(
    new ZipEntryData("file1.txt", "内容1"),
    new ZipEntryData("file2.csv", "内容2")
);
var baos = new ByteArrayOutputStream();
try (var zos = new ZipOutputStream(baos)) {
    for (var entry : entries) {
        zos.putNextEntry(new ZipEntry(entry.name()));
        zos.write(entry.content().getBytes(StandardCharsets.UTF_8));
        zos.closeEntry();
    }
}

Library Comparison

標準 API(java.util.zip)ZIP・GZIP の基本操作で十分な場合。依存ゼロで動き、業務バッチやログ圧縮の大半をカバーできる。パスワード付き ZIP や 7z 形式には対応していない。日本語ファイル名の互換性も注意が必要。
Apache Commons Compresstar.gz、7z、bzip2 など多数のアーカイブ形式を統一的に扱いたいとき。依存が増え、標準 ZIP/GZIP だけなら過剰。業務要件がシンプルなら標準 API で十分。
Zip4jパスワード付き ZIP の作成・展開が必要なとき。AES 暗号化 ZIP にも対応している。パスワード付き ZIP が不要なら導入する理由がない。ライブラリのメンテナンス状況も確認が必要。

注意点

ZipOutputStream で putNextEntry した後、closeEntry を呼ばずに次の putNextEntry を呼ぶと、ZIP ファイルが壊れる可能性がある。エントリごとに必ず closeEntry すること

ZipInputStream で読み込んだ際、entry.getCompressedSize() が -1 を返す場合がある。ストリーム読み込みでは圧縮サイズが事前にわからないケースがあるため、サイズに依存したバッファ確保は避けること

日本語ファイル名を含む ZIP は文字コードの問題が起きやすい。Java 標準の ZipOutputStream は UTF-8 をデフォルトで使うが、Windows のエクスプローラーで文字化けする場合がある

GZIP 圧縮では GZIPOutputStream.close() の前に flush() を呼ばなくても close 内で暗黙に行われるが、try-with-resources を使わない場合は明示的に close しないと圧縮データが不完全になる

巨大ファイルをメモリ上で ByteArrayOutputStream に圧縮すると OutOfMemoryError の原因になる。実運用ではファイルストリームに直接書き出す設計を検討すること

FAQ

ZIP と GZIP の使い分けはどうすればよいですか。

複数ファイルをまとめたい場合は ZIP、単一データの圧縮転送には GZIP が適しています。HTTP レスポンスの圧縮は GZIP が標準的です。

圧縮レベルを指定するにはどうしますか。

ZipOutputStream.setLevel() で Deflater.BEST_SPEED(1)から BEST_COMPRESSION(9)まで指定できます。デフォルトは DEFAULT_COMPRESSION(6相当)で、多くの場合これで十分です。

メモリ上で圧縮するとどの程度のサイズまで安全ですか。

ヒープサイズに依存しますが、目安として数十MB以下であれば ByteArrayOutputStream で問題になることは少ないです。それ以上の場合はファイルストリームへの直接出力を検討してください。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます