Create & Read Zip Files
Working with ZIP archives is a common task — bundling multiple files for download, compressing logs, or packaging resources. Java’s built-in java.util.zip package gives you everything you need to create and read ZIP files without any external library.
The Core Classes
Java’s ZIP support lives in java.util.zip. You will use two classes almost exclusively:
| Class | Purpose |
|---|---|
ZipOutputStream | Wraps an OutputStream to write a ZIP archive |
ZipInputStream | Wraps an InputStream to read/extract a ZIP archive |
ZipEntry | Represents a single file (or directory) inside the archive |
ZipFile | Random-access reading of a ZIP archive by entry name |
Note:
ZipOutputStreamandZipInputStreamfollow the standard Java I/O decorator pattern — they wrap anyOutputStream/InputStream, so you can zip to a file, a byte array, or even a network socket.
Creating a ZIP File
To create a ZIP archive you:
- Open a
FileOutputStreampointing at the target.zipfile. - Wrap it in a
ZipOutputStream. - For each file to add, create a
ZipEntry, callputNextEntry(), copy the file bytes, then callcloseEntry().
import java.io.*;
import java.util.zip.*;
public class CreateZip {
public static void main(String[] args) throws IOException {
// Files to compress (must already exist)
String[] files = {"hello.txt", "data.csv"};
try (ZipOutputStream zos = new ZipOutputStream(
new FileOutputStream("archive.zip"))) {
for (String fileName : files) {
File file = new File(fileName);
try (FileInputStream fis = new FileInputStream(file)) {
// Each entry represents one file inside the ZIP
ZipEntry entry = new ZipEntry(file.getName());
zos.putNextEntry(entry);
byte[] buffer = new byte[1024];
int length;
while ((length = fis.read(buffer)) >= 0) {
zos.write(buffer, 0, length);
}
zos.closeEntry();
}
}
}
System.out.println("archive.zip created successfully.");
}
}
Output:
archive.zip created successfully.
Tip: Always use try-with-resources. Closing a
ZipOutputStreamflushes the central directory record to the end of the file — if you skipclose(), the ZIP will be corrupt.
Preserving Directory Structure
If you want entries like reports/2024/summary.txt inside the ZIP, just use a path string as the entry name:
// Entry name controls the path inside the archive
ZipEntry entry = new ZipEntry("reports/2024/summary.txt");
zos.putNextEntry(entry);
// ... write bytes ...
zos.closeEntry();
The ZIP format stores the path as part of the entry metadata — no actual directories are created on disk inside the archive unless you explicitly add directory entries (entries whose names end with /).
Adding an Explicit Directory Entry
// A directory entry has a trailing slash and zero bytes
ZipEntry dirEntry = new ZipEntry("reports/2024/");
zos.putNextEntry(dirEntry);
zos.closeEntry(); // no bytes to write
Setting Compression Level
By default ZipOutputStream uses Deflate compression. You can control the compression level (0 = no compression, 9 = maximum) with setLevel():
import java.io.*;
import java.util.zip.*;
public class CompressedZip {
public static void main(String[] args) throws IOException {
try (ZipOutputStream zos = new ZipOutputStream(
new FileOutputStream("compressed.zip"))) {
zos.setLevel(Deflater.BEST_COMPRESSION); // level 9
ZipEntry entry = new ZipEntry("notes.txt");
zos.putNextEntry(entry);
zos.write("Hello, compressed world!".getBytes());
zos.closeEntry();
}
System.out.println("Done.");
}
}
You can also store entries without compression using ZipEntry.STORED, but you must then set the entry’s size, compressedSize, and crc manually before calling putNextEntry() — usually not worth the hassle unless you need random-access speed.
Reading (Extracting) a ZIP File
Use ZipInputStream to iterate through entries one by one:
import java.io.*;
import java.util.zip.*;
public class ReadZip {
public static void main(String[] args) throws IOException {
try (ZipInputStream zis = new ZipInputStream(
new FileInputStream("archive.zip"))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
System.out.println("Extracting: " + entry.getName()
+ " (" + entry.getSize() + " bytes)");
// Write each entry out to disk
try (FileOutputStream fos = new FileOutputStream(entry.getName())) {
byte[] buffer = new byte[1024];
int length;
while ((length = zis.read(buffer)) >= 0) {
fos.write(buffer, 0, length);
}
}
zis.closeEntry();
}
}
}
}
Output:
Extracting: hello.txt (26 bytes)
Extracting: data.csv (142 bytes)
Warning: Before writing extracted files to disk, always sanitize
entry.getName()to prevent Zip Slip — a path-traversal vulnerability where a malicious archive contains entries like../../etc/passwd. Check that the resolved path stays within your target directory.
Zip Slip Prevention
File targetDir = new File("output/");
File outFile = new File(targetDir, entry.getName()).getCanonicalFile();
if (!outFile.toPath().startsWith(targetDir.getCanonicalFile().toPath())) {
throw new SecurityException("Zip Slip detected: " + entry.getName());
}
Random-Access Reading with ZipFile
ZipInputStream reads entries sequentially. When you need to jump directly to a named entry, use ZipFile instead:
import java.io.*;
import java.util.zip.*;
public class RandomAccessZip {
public static void main(String[] args) throws IOException {
try (ZipFile zipFile = new ZipFile("archive.zip")) {
// Get a specific entry by name
ZipEntry entry = zipFile.getEntry("hello.txt");
if (entry != null) {
try (InputStream is = zipFile.getInputStream(entry);
BufferedReader reader = new BufferedReader(
new InputStreamReader(is))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
}
}
}
ZipFile reads the central directory (stored at the end of the ZIP) into memory first, giving O(1) lookup by name rather than O(n) sequential scan.
Listing All Entries Without Extracting
import java.util.zip.*;
public class ListZip {
public static void main(String[] args) throws IOException {
try (ZipFile zipFile = new ZipFile("archive.zip")) {
zipFile.entries().asIterator().forEachRemaining(entry ->
System.out.printf("%-30s %8d bytes%n",
entry.getName(), entry.getSize())
);
}
}
}
Output:
hello.txt 26 bytes
data.csv 142 bytes
Under the Hood
ZIP Format Structure
A ZIP file is not simply a sequence of compressed streams — it has a specific binary structure:
- Local file headers — appear before each entry’s compressed data, holding the file name, compression method, sizes, and CRC-32.
- Central directory — a catalogue of all entries written at the very end of the file. This is why
ZipFilecan do fast random access without scanning the whole archive. - End of central directory record (EOCD) — a small fixed-size record after the central directory that tells readers where the central directory starts.
ZipInputStream reads local headers sequentially (it never sees the central directory). ZipFile seeks to the EOCD first, loads the central directory, and builds an in-memory index.
Deflate Compression
The default DEFLATED method is the same algorithm used in gzip and PNG. It combines LZ77 (back-references to earlier data) with Huffman coding (shorter codes for frequent symbols). Java delegates to the native zlib library via JNI, so compression and decompression are fast even for large files.
Buffering Tip
Wrap your FileInputStream / FileOutputStream in a BufferedInputStream or BufferedOutputStream when dealing with many small files. Each zos.write() call eventually hits the underlying stream; buffering reduces system-call overhead significantly.
// Buffered for better performance
try (ZipOutputStream zos = new ZipOutputStream(
new BufferedOutputStream(new FileOutputStream("archive.zip")))) {
// ...
}
NIO.2 Alternative
Java 7+ ships with a ZipFileSystem accessible through the NIO.2 API (java.nio.file), letting you treat a ZIP archive as a virtual filesystem:
import java.net.URI;
import java.nio.file.*;
public class NioZip {
public static void main(String[] args) throws Exception {
URI uri = URI.create("jar:file:/tmp/archive.zip");
try (FileSystem fs = FileSystems.newFileSystem(uri,
java.util.Map.of("create", "true"))) {
Path inside = fs.getPath("/hello.txt");
Files.writeString(inside, "Hello from NIO.2!");
}
System.out.println("Written via NIO.2 ZipFileSystem.");
}
}
This approach integrates seamlessly with Files.copy(), Files.walk(), and other NIO.2 utilities — see NIO.2: Path & Files for more.
Related Topics
- NIO.2: Path & Files — modern file operations that integrate with
ZipFileSystemfor treating archives as virtual directories - BufferedInputStream — wrap your streams in a buffer to dramatically speed up ZIP creation for many small files
- BufferedOutputStream — buffer the output side when writing ZIP archives to disk
- File Handling — the broader context of reading, writing, and managing files in Java
- Serialization — another approach to persisting Java objects, often combined with ZIP streams for compact storage
- Java I/O — overview of Java’s entire I/O framework and where ZIP streams fit in