Contents
- Creating a ZIP Archive
- Reading a ZIP Archive
- GZIP Single-file Compression
- NIO ZipFileSystem
- Practical Patterns
ZipOutputStream wraps any OutputStream and writes entries in the ZIP format. For each file to add, call putNextEntry(new ZipEntry(name)) to begin the entry, write the file's bytes, then call closeEntry() to finalise it. Repeat for each additional file. Closing ZipOutputStream writes the central directory and finalises the archive — always wrap it in try-with-resources. Wrap the underlying FileOutputStream in a BufferedOutputStream to improve throughput. The compression level can be adjusted from 0 (store only) to 9 (best compression).
import java.io.*;
import java.util.zip.*;
import java.nio.file.*;
// Create a ZIP with multiple files
try (ZipOutputStream zos = new ZipOutputStream(
new BufferedOutputStream(new FileOutputStream("archive.zip")))) {
// Set compression level (0=store, 9=best compression)
zos.setLevel(Deflater.BEST_COMPRESSION); // default is 6
// Add a file entry from disk
addFileToZip(zos, Path.of("report.csv"), "data/report.csv");
addFileToZip(zos, Path.of("summary.txt"), "summary.txt");
// Add an in-memory entry (no source file needed)
ZipEntry memEntry = new ZipEntry("meta/info.json");
memEntry.setComment("Metadata file");
zos.putNextEntry(memEntry);
zos.write("{\"version\": 1}".getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
}
void addFileToZip(ZipOutputStream zos, Path file, String entryName) throws IOException {
ZipEntry entry = new ZipEntry(entryName);
// Optional: set modification time
entry.setLastModifiedTime(Files.getLastModifiedTime(file));
zos.putNextEntry(entry);
Files.copy(file, zos); // efficient — streams file bytes directly into zip
zos.closeEntry();
}
// Store mode — add file without compression (useful for already-compressed content)
ZipEntry stored = new ZipEntry("image.png");
stored.setMethod(ZipEntry.STORED);
stored.setSize(Files.size(imagePath));
stored.setCrc(computeCrc(imagePath)); // required for STORED entries
zos.putNextEntry(stored);
Files.copy(imagePath, zos);
zos.closeEntry();
ZipInputStream wraps a FileInputStream and iterates over entries sequentially. Call getNextEntry() in a loop: it returns the next ZipEntry with metadata (name, size, modification time) or null when there are no more entries. Read the entry's bytes normally from the stream, then call closeEntry() before advancing to the next one. For random access by entry name — fetching a single file from a large archive — use ZipFile instead, which reads the central directory and locates entries directly without sequential iteration.
import java.util.zip.*;
// Read all entries from a ZIP file
try (ZipInputStream zis = new ZipInputStream(
new BufferedInputStream(new FileInputStream("archive.zip")))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.isDirectory()) {
System.out.println("DIR: " + entry.getName());
zis.closeEntry();
continue;
}
System.out.printf("FILE: %-30s size=%d%n", entry.getName(), entry.getSize());
// Extract to disk
Path outPath = Path.of("extracted", entry.getName());
Files.createDirectories(outPath.getParent());
Files.copy(zis, outPath, StandardCopyOption.REPLACE_EXISTING);
// Note: do NOT close zis between entries — just closeEntry()
zis.closeEntry();
}
}
// ZipFile — random access to entries by name (doesn't need sequential iteration)
try (ZipFile zf = new ZipFile("archive.zip")) {
System.out.println("Total entries: " + zf.size());
// Access a specific entry by name
ZipEntry entry = zf.getEntry("data/report.csv");
if (entry != null) {
try (InputStream in = zf.getInputStream(entry)) {
String content = new String(in.readAllBytes(), StandardCharsets.UTF_8);
System.out.println(content);
}
}
// List all entries
zf.entries().asIterator().forEachRemaining(e ->
System.out.println(e.getName() + " (" + e.getCompressedSize() + " bytes)"));
}
Always validate ZIP entry paths before extracting to prevent Zip Slip attacks — a malicious ZIP can contain entries like ../../etc/passwd that escape the destination directory. Normalize the output path and verify it starts within your target directory.
GZIP is a single-stream compression format — it compresses one byte sequence, not a collection of named files like ZIP. GZIPOutputStream and GZIPInputStream wrap any stream and apply DEFLATE compression transparently. GZIP is commonly used to compress log files (producing .gz files), HTTP response bodies, and API payloads where bandwidth matters. Because it wraps any stream, it can also compress in-memory byte arrays without touching the filesystem.
import java.util.zip.*;
// Compress a file with GZIP
void gzipFile(String src, String dst) throws IOException {
try (InputStream in = new BufferedInputStream(new FileInputStream(src));
OutputStream out = new GZIPOutputStream(new FileOutputStream(dst))) {
in.transferTo(out); // Java 9+ — copies all bytes
}
}
// Decompress a GZIP file
void gunzipFile(String src, String dst) throws IOException {
try (InputStream in = new GZIPInputStream(new FileInputStream(src));
OutputStream out = new BufferedOutputStream(new FileOutputStream(dst))) {
in.transferTo(out);
}
}
// In-memory GZIP — compress a String to byte[]
byte[] compress(String text) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (GZIPOutputStream gz = new GZIPOutputStream(baos)) {
gz.write(text.getBytes(StandardCharsets.UTF_8));
}
return baos.toByteArray();
}
// Decompress byte[] back to String
String decompress(byte[] compressed) throws IOException {
try (GZIPInputStream gz = new GZIPInputStream(new ByteArrayInputStream(compressed))) {
return new String(gz.readAllBytes(), StandardCharsets.UTF_8);
}
}
// Usage: compress HTTP request/response bodies
byte[] compressedBody = compress(jsonResponse);
System.out.println("Original: " + jsonResponse.length() + " bytes");
System.out.println("Compressed: " + compressedBody.length + " bytes");
System.out.println("Ratio: " + (100 - 100 * compressedBody.length / jsonResponse.length()) + "% smaller");
FileSystems.newFileSystem() with a ZIP path mounts the archive as a virtual filesystem, allowing you to use the standard Files API — Files.readAllLines(), Files.writeString(), Files.walk() — directly on entries inside the ZIP without extracting it first. Writes are buffered and committed only when the FileSystem is closed, so always use try-with-resources. To create a new ZIP rather than open an existing one, pass Map.of("create", "true") as the environment. This is the most ergonomic approach for complex read/write operations on ZIP archives in modern Java.
import java.nio.file.*;
// ZipFileSystem — treat a ZIP as a filesystem; read/write in-place
// No need to extract first
// Open existing ZIP
try (FileSystem fs = FileSystems.newFileSystem(Path.of("archive.zip"))) {
// List all files in the ZIP
Files.walk(fs.getPath("/"))
.filter(p -> !Files.isDirectory(p))
.forEach(System.out::println);
// Read a file inside the ZIP
Path entry = fs.getPath("/data/report.csv");
List<String> lines = Files.readAllLines(entry, StandardCharsets.UTF_8);
// Write/overwrite a file inside the ZIP
Path meta = fs.getPath("/meta/info.json");
Files.writeString(meta, "{\"version\": 2}");
}
// Changes are committed when the FileSystem is closed
// Create a new ZIP using ZipFileSystem
Map<String, String> env = Map.of("create", "true");
try (FileSystem fs = FileSystems.newFileSystem(
URI.create("jar:file:/tmp/new.zip"), env)) {
// Create directories and files inside the new ZIP
Files.createDirectory(fs.getPath("/data"));
Files.writeString(fs.getPath("/data/hello.txt"), "Hello, ZIP!");
Files.copy(Path.of("logo.png"), fs.getPath("/logo.png"));
}
// /tmp/new.zip created with the specified structure
The NIO ZipFileSystem API is the most ergonomic way to work with ZIP archives in modern Java. Use it when you need to read or modify specific entries without extracting the whole archive. Remember to close the FileSystem to commit changes.
Recurring ZIP use cases include compressing HTTP responses on the fly, archiving log directories for rotation, and packaging deployment artifacts. Always close streams in try-with-resources — an unclosed ZipOutputStream produces a truncated, unreadable archive. For large files, stream bytes chunk-by-chunk rather than loading the entire file into a byte array; Files.copy(path, zipOutputStream) does this efficiently. When extracting, always validate entry paths against the target directory to prevent Zip Slip attacks where malicious ../ sequences escape the destination.
// Pattern 1: Compress a directory recursively into a ZIP
void zipDirectory(Path sourceDir, Path zipFile) throws IOException {
try (ZipOutputStream zos = new ZipOutputStream(
new BufferedOutputStream(Files.newOutputStream(zipFile)))) {
Files.walk(sourceDir)
.filter(p -> !Files.isDirectory(p))
.forEach(p -> {
try {
String entryName = sourceDir.relativize(p).toString()
.replace(File.separatorChar, '/'); // ZIP uses /
zos.putNextEntry(new ZipEntry(entryName));
Files.copy(p, zos);
zos.closeEntry();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
}
// Pattern 2: Read a ZIP file's manifest (if it has one — e.g., a JAR)
try (ZipFile jar = new ZipFile("app.jar")) {
ZipEntry manifest = jar.getEntry("META-INF/MANIFEST.MF");
if (manifest != null) {
String content = new String(jar.getInputStream(manifest).readAllBytes());
System.out.println(content);
}
}
// Pattern 3: Zip Slip prevention
Path targetDir = Path.of("output").toAbsolutePath().normalize();
ZipEntry entry = new ZipEntry("../../etc/passwd"); // malicious
Path resolved = targetDir.resolve(entry.getName()).normalize();
if (!resolved.startsWith(targetDir)) {
throw new SecurityException("Zip Slip detected: " + entry.getName());
}