こんにちは。開発ブログ運営担当のktです。

今回は出力したファイルをZipに圧縮する必要があったのでその方法を調査しました。
まずは、定番になったChatGPT先生に「javaでテキストファイルをzip圧縮するコードを教えてください。」と聞いてみました。
以下回答のコードです。

import java.io.*;
import java.util.zip.*;

public class ZipFileExample {
    public static void main(String[] args) {
        String sourceFile = "input.txt";
        String zipFile = "output.zip";

        try {
            // 入力ファイルのストリームを作成
            FileInputStream fis = new FileInputStream(sourceFile);

            // 出力ZIPファイルのストリームを作成
            FileOutputStream fos = new FileOutputStream(zipFile);

            // ZIP出力ストリームを作成
            ZipOutputStream zos = new ZipOutputStream(fos);

            // ZIPエントリを作成
            ZipEntry zipEntry = new ZipEntry(sourceFile);

            // ZIPエントリを追加
            zos.putNextEntry(zipEntry);

            // 入力ファイルを読み込んでZIPに書き込む
            byte[] buffer = new byte[1024];
            int length;
            while ((length = fis.read(buffer)) > 0) {
                zos.write(buffer, 0, length);
            }

            // ストリームをクローズ
            fis.close();
            zos.closeEntry();
            zos.close();

            System.out.println("ファイルが正常に圧縮されました。");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

…なんか古いコードですね。
このコードでも正常にファイルを圧縮することができましたが、try-with-resources文も使っておらず、NIO2も利用していません。
Google検索で「java zip」のワードで検索し上位のサイトをいくつか見てみましたが、似たようなコードが多かったです。
そこで今回はこのコードをもう少し現代的にしたいと思います。

環境

Windows 11
Java 17
Eclipse 2022

まずは現代的に書き換えたコードです。

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class NewZipOutputSample {
	public static void main(String[] args) {
		//(1)
		Path sourcePath = Path.of("file-in/large_text_file.txt");
		Path zipPath = Path.of("file-out/output.zip");
		
		//(2)(3)
		try(InputStream is = Files.newInputStream(sourcePath);
			OutputStream os = Files.newOutputStream(zipPath);
			ZipOutputStream zos = new ZipOutputStream(os);
			) {
			
			ZipEntry zipEntry = new ZipEntry(sourcePath.getFileName().toString());
			zos.putNextEntry(zipEntry);
			//(4)
			is.transferTo(zos);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

(1)Java 11から導入された’Path.of’メソッドを利用してファイルのパスを指定しています。
今回の場合はパスを指定しているだけなのでそんなに違いはないですが、’Path’クラスはプラットフォームに依存しないコードを書くことができ、ファイル操作の多くの面で便利なのでおすすめです。

(2)続いて、try-with-resources文にすることでclose処理が自動になるので、各Streamをcloseするコードが不要になりました。

(3)次に、’FileInputStream’クラスを’Files.newInputStream’に変更しています。
通常、’FileInputStream’を利用するときは下記のように’BufferedInputStream’でラッピングして利用します。

BufferedInputStream bis = new BufferedInputStream(new FileInputStream(sourceFile));

バッファリングでデータを効率的に読み取りパフォーマンスが向上するためですが、コードが読みにくくなるのは否めません。
実際に’FileInputStream’を’BufferedInputStream’でラッピングした方がパフォーマンスがいいようですが、コードの簡潔さを取るかパフォーマンスを重視するかの違いですね。
今回は’Files.newInputStream’を利用しておきます。
‘FileOutputStream’クラスも同様に’Files.newOutputStream’に変更しておきます。

(4)最後に、whileループでバイト単位で読みこんで’ZipOutputStream’に出力していたコードを、
Java 9から導入された’transferTo’メソッドに変更しています。
この’transferTo’メソッドのソースコードを見てみます。

public long transferTo(OutputStream out) throws IOException {
    Objects.requireNonNull(out, "out");
    long transferred = 0;
    byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
    int read;
    while ((read = this.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
        out.write(buffer, 0, read);
        transferred += read;
    }
    return transferred;
}

変更前のwhileループとほぼ同じ内容になっています。
同じ処理なら自前で実装せずにあるものを利用すればいいですね。
しかし’Files.newInputStream’メソッドが返す’InputStream’はバッファリングされていません。
‘BufferedInputStream’でラッピングした方が良さそうですが、
transferToメソッドを利用する場合は性能はあまり変わらないそうなのでラッピングしません。
参考サイト⇒https://retheviper.github.io/posts/java-file-copy/

まとめ

最初のコードより簡潔になったと思います。
コードを簡潔にしておくことは、読みやすくて処理も理解しやすく、不具合の原因も見つけやすくなります。
これからも心掛けていこうと思います。