Java has two I/O subsystems: the original java.io (blocking, stream-based) and java.nio (buffers, channels, selectors, non-blocking). The modern java.nio.file package (Java 7+) replaced the clunky File class with the expressive Path and Files APIs. Understanding both is essential — java.io is simpler for sequential processing, NIO is mandatory for high-performance network servers and large file handling.
Step 1 — The java.io Stream Hierarchy
java.io splits I/O into byte streams (InputStream/OutputStream) for binary data and character streams (Reader/Writer) for text. Decorator streams wrap a base stream to add capabilities like buffering, data conversion, or compression. Always close streams — use try-with-resources.
Core java.io classes
- FileInputStream / FileOutputStream: raw byte-level reading and writing of files.
- BufferedInputStream / BufferedOutputStream: wraps a stream to add buffering — reduces system calls dramatically. Default buffer is 8KB.
- DataInputStream / DataOutputStream: reads and writes Java primitives (int, double, boolean) in a portable binary format.
- FileReader / FileWriter: character-level file I/O using the platform's default charset. Use InputStreamReader/OutputStreamWriter with explicit charset instead.
- BufferedReader / BufferedWriter: adds buffering to character streams. BufferedReader.readLine() is the standard way to read text files line by line.
- PrintWriter / PrintStream: convenient print/println/printf methods. System.out is a PrintStream.
import java.io.*;
import java.nio.charset.StandardCharsets;
public class BasicIO {
// ============================================================
// Writing text to a file
// ============================================================
static void writeTextFile(String path) throws IOException {
// Always specify charset explicitly — never rely on platform default
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(path), StandardCharsets.UTF_8))) {
writer.write("Line 1: Hello, Java I/O");
writer.newLine(); // platform-independent line separator
writer.write("Line 2: Buffered for performance");
writer.newLine();
writer.write("Line 3: Always use try-with-resources");
// writer.flush() is NOT needed — close() calls flush() first
} // auto-close: flushes buffer and closes file
}
// ============================================================
// Reading text from a file — line by line
// ============================================================
static void readTextFile(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8))) {
String line;
int lineNum = 1;
while ((line = reader.readLine()) != null) { // returns null at EOF
System.out.println(lineNum++ + ": " + line);
}
}
}
// ============================================================
// Writing binary data with DataOutputStream
// ============================================================
static void writeBinaryData(String path) throws IOException {
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream(path)))) {
dos.writeInt(42); // writes 4 bytes (big-endian)
dos.writeDouble(3.14159); // writes 8 bytes
dos.writeBoolean(true); // writes 1 byte
dos.writeUTF("Hello"); // writes length-prefixed modified UTF-8
}
}
// ============================================================
// Reading binary data with DataInputStream
// ============================================================
static void readBinaryData(String path) throws IOException {
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(new FileInputStream(path)))) {
int i = dis.readInt(); // must read in same order as written
double d = dis.readDouble();
boolean b = dis.readBoolean();
String s = dis.readUTF();
System.out.printf("int=%d double=%.5f bool=%b str=%s%n", i, d, b, s);
}
}
// ============================================================
// Reading classpath resources (common in Spring apps)
// ============================================================
static void readResource(String resourceName) throws IOException {
try (InputStream is = BasicIO.class.getClassLoader().getResourceAsStream(resourceName);
BufferedReader reader = new BufferedReader(
new InputStreamReader(is, StandardCharsets.UTF_8))) {
reader.lines().forEach(System.out::println);
}
}
public static void main(String[] args) throws IOException {
writeTextFile("output.txt");
readTextFile("output.txt");
writeBinaryData("data.bin");
readBinaryData("data.bin");
}
}Step 2 — Object Serialization
Serialization converts a Java object to a byte stream for storage or network transfer. Deserialization reconstructs the object. A class must implement Serializable (a marker interface). The JVM generates a serialVersionUID to version the class — always declare it explicitly to control compatibility.
import java.io.*;
import java.util.List;
// Must implement Serializable to be serializable
public class SerializationExample implements Serializable {
// Always declare explicitly — if you don't, Java computes one from class structure
// Adding/removing fields changes the computed UID, breaking deserialization of old data
private static final long serialVersionUID = 1L;
private String name;
private int age;
private transient String password; // transient: NOT serialized (sensitive data, derived fields)
private static String appVersion = "1.0"; // static fields are NOT serialized
public SerializationExample(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}
// Custom serialization: called by ObjectOutputStream
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // serialize non-transient fields normally
// You can write additional encrypted/derived data here
oos.writeObject("encrypted:" + password.hashCode());
}
// Custom deserialization: called by ObjectInputStream
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // deserialize non-transient fields
String encryptedPwd = (String) ois.readObject();
// Restore derived state that wasn't directly serialized
this.password = "[restored]";
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + ", password='" + password + "'}";
}
// ============================================================
// Serialize object to file
// ============================================================
static void serialize(Object obj, String path) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream(path)))) {
oos.writeObject(obj);
}
}
// ============================================================
// Deserialize object from file
// ============================================================
@SuppressWarnings("unchecked")
static <T> T deserialize(String path) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(new FileInputStream(path)))) {
return (T) ois.readObject();
}
}
public static void main(String[] args) throws Exception {
SerializationExample user = new SerializationExample("Alice", 30, "secret123");
System.out.println("Before: " + user);
serialize(user, "user.ser");
SerializationExample restored = deserialize("user.ser");
System.out.println("After: " + restored);
// password will be "[restored]" — not the original value
// appVersion (static) is not restored from file but from class definition
// Serializing collections
List<String> list = List.of("a", "b", "c"); // List.of() returns Serializable list
serialize(list, "list.ser");
List<String> restoredList = deserialize("list.ser");
System.out.println("List: " + restoredList);
}
}Step 3 — Modern File API: Path and Files (java.nio.file)
The java.nio.file package (Java 7+) replaced java.io.File with the immutable Path interface and the utility class Files. Path is just a representation of a location; Files contains the actual I/O operations. This API is far more expressive, supports symbolic links, and provides atomic operations.
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.util.List;
import java.util.stream.Stream;
public class PathAndFiles {
public static void main(String[] args) throws IOException {
// ============================================================
// Path construction and manipulation
// ============================================================
Path home = Path.of("/home/user"); // Java 11+
Path docs = home.resolve("documents"); // /home/user/documents
Path file = docs.resolve("report.txt"); // /home/user/documents/report.txt
Path parent = file.getParent(); // /home/user/documents
Path name = file.getFileName(); // report.txt
Path abs = file.toAbsolutePath(); // resolve against cwd if relative
Path norm = Path.of("/a/b/../c").normalize(); // /a/c
// Relativize: path from one to another
Path rel = home.relativize(file); // documents/report.txt
System.out.println("parent: " + parent);
System.out.println("name: " + name);
System.out.println("rel: " + rel);
System.out.println("norm: " + norm);
// ============================================================
// Reading and writing files
// ============================================================
Path testFile = Path.of("test.txt");
// Write all lines at once (simple, buffered internally)
Files.writeString(testFile, "Hello\nWorld\nJava NIO", StandardCharsets.UTF_8);
// Read entire file as String
String content = Files.readString(testFile, StandardCharsets.UTF_8);
System.out.println(content);
// Read all lines as List<String>
List<String> lines = Files.readAllLines(testFile, StandardCharsets.UTF_8);
System.out.println("Lines: " + lines.size());
// Read as Stream<String> — lazy, efficient for large files
try (Stream<String> lineStream = Files.lines(testFile, StandardCharsets.UTF_8)) {
long count = lineStream.filter(l -> l.contains("Java")).count();
System.out.println("Lines with 'Java': " + count);
}
// Append to a file
Files.writeString(testFile, "\nAppended line",
StandardCharsets.UTF_8,
StandardOpenOption.APPEND); // OpenOption controls create/append/truncate behavior
// ============================================================
// Directory operations
// ============================================================
Path dir = Path.of("mydir/sub1/sub2");
Files.createDirectories(dir); // creates ALL missing parent directories
// Copy file
Path copy = Path.of("test_copy.txt");
Files.copy(testFile, copy, StandardCopyOption.REPLACE_EXISTING);
// Move/rename file (atomic on same filesystem)
Path moved = Path.of("test_moved.txt");
Files.move(copy, moved, StandardCopyOption.ATOMIC_MOVE);
// Delete
Files.deleteIfExists(moved); // no exception if file doesn't exist
// Files.delete(path); // throws NoSuchFileException if missing
// ============================================================
// Walking the directory tree
// ============================================================
Files.createFile(dir.resolve("a.txt"));
Files.createFile(dir.resolve("b.java"));
// Walk: depth-first traversal
try (Stream<Path> walk = Files.walk(Path.of("mydir"))) {
walk.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println); // prints mydir/sub1/sub2/b.java
}
// List: only immediate children (not recursive)
try (Stream<Path> list = Files.list(Path.of("mydir"))) {
list.forEach(System.out::println);
}
// Find: walk with BiPredicate filter
try (Stream<Path> found = Files.find(
Path.of("mydir"), 10, // max depth 10
(path, attrs) -> attrs.isRegularFile() && path.toString().endsWith(".txt"))) {
found.forEach(System.out::println);
}
// ============================================================
// File metadata
// ============================================================
System.out.println("Exists: " + Files.exists(testFile));
System.out.println("Is file: " + Files.isRegularFile(testFile));
System.out.println("Is dir: " + Files.isDirectory(testFile));
System.out.println("Size bytes: " + Files.size(testFile));
System.out.println("Last mod: " + Files.getLastModifiedTime(testFile));
System.out.println("Hidden: " + Files.isHidden(testFile));
// Cleanup
Files.deleteIfExists(testFile);
Files.walk(Path.of("mydir"))
.sorted(java.util.Comparator.reverseOrder()) // delete deepest first
.forEach(p -> { try { Files.delete(p); } catch (IOException e) { } });
}
}Step 4 — WatchService: File System Events
WatchService monitors a directory for file system events (create, modify, delete) without polling. The OS notifies the JVM. Used in hot-reload systems, configuration file monitoring, and build tools.
import java.nio.file.*;
import java.io.IOException;
public class WatchServiceExample {
public static void main(String[] args) throws IOException, InterruptedException {
Path watchDir = Path.of("watched");
Files.createDirectories(watchDir);
// Create a watch service (backed by OS-level inotify/FSEvents/ReadDirectoryChanges)
WatchService watcher = FileSystems.getDefault().newWatchService();
// Register the directory for specific events
watchDir.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE, // new file/dir created
StandardWatchEventKinds.ENTRY_MODIFY, // file modified
StandardWatchEventKinds.ENTRY_DELETE // file/dir deleted
);
System.out.println("Watching: " + watchDir.toAbsolutePath());
System.out.println("Create/modify/delete files in that directory...");
// Monitor in a separate thread so main app is not blocked
Thread watchThread = new Thread(() -> {
try {
while (true) {
// poll(): non-blocking; take(): blocking; poll(timeout): timed blocking
WatchKey key = watcher.take(); // blocks until an event occurs
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
// OVERFLOW means events were lost (too many events at once)
if (kind == StandardWatchEventKinds.OVERFLOW) {
System.out.println("WARNING: some events were lost");
continue;
}
// The context is the filename that triggered the event
@SuppressWarnings("unchecked")
WatchEvent<Path> ev = (WatchEvent<Path>) event;
Path filename = ev.context(); // just the filename, not full path
Path fullPath = watchDir.resolve(filename);
System.out.printf("Event: %-20s -> %s%n",
kind.name(), fullPath);
// Example: reload config when config.properties changes
if (kind == StandardWatchEventKinds.ENTRY_MODIFY
&& filename.toString().equals("config.properties")) {
System.out.println("Config changed — reloading...");
// reloadConfig(fullPath);
}
}
// CRITICAL: reset the key — without this, no further events are delivered
boolean valid = key.reset();
if (!valid) {
System.out.println("Watch key no longer valid (directory deleted?)");
break;
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Watcher thread interrupted");
}
});
watchThread.setDaemon(true);
watchThread.start();
// Simulate file events for the demo
Thread.sleep(500);
Files.writeString(watchDir.resolve("hello.txt"), "created");
Thread.sleep(200);
Files.writeString(watchDir.resolve("hello.txt"), "modified",
StandardOpenOption.APPEND);
Thread.sleep(200);
Files.delete(watchDir.resolve("hello.txt"));
Thread.sleep(500);
watcher.close();
Files.deleteIfExists(watchDir);
}
}Step 5 — NIO Channels and Buffers
NIO (New I/O) introduces a different I/O model: data moves between channels and buffers. A Buffer is a fixed-size memory block; a Channel is a bidirectional conduit to a file or socket. The key difference from java.io: NIO channels can be non-blocking, and you can transfer data between channels directly without copying to userspace.
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class ChannelBufferExample {
// ============================================================
// ByteBuffer fundamentals
// ============================================================
static void bufferDemo() {
// Allocate a buffer with 64-byte capacity
ByteBuffer buf = ByteBuffer.allocate(64);
// ByteBuffer.allocateDirect(64) — off-heap memory: faster for channel I/O, no GC
// Buffer has three key positions:
// capacity: total size (never changes)
// limit: end of readable/writable data
// position: current read/write cursor
// --- Writing into buffer ---
buf.putInt(42); // writes 4 bytes, position moves to 4
buf.putDouble(3.14); // writes 8 bytes, position moves to 12
buf.put("Hi".getBytes(StandardCharsets.UTF_8)); // writes 2 bytes, position 14
// --- Switch from write mode to read mode ---
buf.flip(); // sets limit=position, position=0
// Now: position=0, limit=14, ready to read 14 bytes
int i = buf.getInt(); // reads 4 bytes, position=4
double d = buf.getDouble(); // reads 8 bytes, position=12
byte[] text = new byte[2];
buf.get(text); // reads 2 bytes, position=14
System.out.printf("int=%d double=%.2f text=%s%n", i, d, new String(text));
// compact(): preserve unread bytes, switch back to write mode
buf.compact(); // copies unread bytes to start, position=remaining, limit=capacity
// clear(): reset without erasing data (position=0, limit=capacity)
buf.clear();
// rewind(): re-read same data (position=0, limit unchanged)
// mark() / reset(): save and restore position
}
// ============================================================
// FileChannel: reading and writing files via NIO
// ============================================================
static void fileChannelDemo() throws IOException {
Path file = Path.of("channel_demo.txt");
// Writing with FileChannel
try (FileChannel fc = FileChannel.open(file,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)) {
ByteBuffer buf = ByteBuffer.wrap(
"Hello from FileChannel!\nSecond line\n".getBytes(StandardCharsets.UTF_8));
while (buf.hasRemaining()) {
fc.write(buf); // write returns bytes written — loop in case of partial write
}
fc.force(true); // flush to disk (fsync) — true=also sync metadata
}
// Reading with FileChannel
try (FileChannel fc = FileChannel.open(file, StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(1024);
StringBuilder sb = new StringBuilder();
int bytesRead;
while ((bytesRead = fc.read(buf)) != -1) { // -1 = EOF
buf.flip();
sb.append(StandardCharsets.UTF_8.decode(buf));
buf.clear();
}
System.out.println("Read: " + sb);
}
// Channel-to-channel transfer (zero-copy: data stays in kernel space)
Path dest = Path.of("channel_copy.txt");
try (FileChannel src = FileChannel.open(file, StandardOpenOption.READ);
FileChannel dst = FileChannel.open(dest,
StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
long transferred = src.transferTo(0, src.size(), dst);
System.out.println("Transferred " + transferred + " bytes");
}
// Cleanup
Files.deleteIfExists(file);
Files.deleteIfExists(dest);
}
// ============================================================
// Memory-mapped files: map file into virtual memory
// OS pages the data in on demand — fastest for large random-access files
// ============================================================
static void memoryMappedFile() throws IOException {
Path file = Path.of("mapped.bin");
int size = 1024 * 1024; // 1MB
try (FileChannel fc = FileChannel.open(file,
StandardOpenOption.CREATE,
StandardOpenOption.READ,
StandardOpenOption.WRITE)) {
// Map the file into memory
MappedByteBuffer mapped = fc.map(
FileChannel.MapMode.READ_WRITE, // read+write mapping
0, // offset in file
size // length to map
);
// Write directly to memory (changes reflect in file automatically)
for (int i = 0; i < 1000; i++) {
mapped.putInt(i * 4, i * i); // absolute put at specific offset — no flip needed
}
// Read back
System.out.println("mapped[10] = " + mapped.getInt(10 * 4)); // 100
mapped.force(); // flush to disk
}
Files.deleteIfExists(file);
}
public static void main(String[] args) throws IOException {
bufferDemo();
fileChannelDemo();
memoryMappedFile();
}
}Step 6 — Non-Blocking Network I/O with Selector
Traditional java.net (ServerSocket/Socket) is one-thread-per-connection: blocking, doesn't scale to thousands of simultaneous connections. NIO Selector lets one thread monitor many channels for I/O readiness events, then handle only those that are ready. This is the foundation of high-performance servers like Netty.
import java.io.IOException;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
public class NioServer {
public static void main(String[] args) throws IOException {
// ============================================================
// Single-thread NIO echo server
// Handles multiple clients without creating a thread per client
// ============================================================
// Open a selector — the OS-level multiplexer
Selector selector = Selector.open();
// Open a server socket channel (non-blocking)
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false); // MUST be non-blocking for Selector
serverChannel.bind(new InetSocketAddress(8080));
// Register server channel with selector — interested in ACCEPT events
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO Echo Server listening on port 8080");
ByteBuffer buffer = ByteBuffer.allocate(256);
while (true) {
// select(): blocks until at least one channel is ready
// selectNow(): non-blocking, returns 0 immediately if nothing ready
// select(timeout): timed block
int readyCount = selector.select(); // blocks here
if (readyCount == 0) continue;
// Get the set of keys with ready channels
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove(); // CRITICAL: remove from selected set after handling
if (!key.isValid()) continue;
if (key.isAcceptable()) {
// Server channel is ready to accept a new connection
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept(); // non-blocking: returns immediately
if (client != null) {
client.configureBlocking(false);
// Register client for READ events
client.register(selector, SelectionKey.OP_READ);
System.out.println("Accepted: " + client.getRemoteAddress());
}
} else if (key.isReadable()) {
// A client channel has data ready to read
SocketChannel client = (SocketChannel) key.channel();
buffer.clear();
int bytesRead;
try {
bytesRead = client.read(buffer);
} catch (IOException e) {
// Client disconnected unexpectedly
key.cancel();
client.close();
continue;
}
if (bytesRead == -1) {
// Client closed connection gracefully
System.out.println("Client disconnected: " + client.getRemoteAddress());
key.cancel();
client.close();
continue;
}
// Echo back what we received
buffer.flip();
String received = StandardCharsets.UTF_8.decode(buffer).toString().trim();
System.out.println("Received: " + received);
buffer.rewind(); // rewind to re-read what we just read
while (buffer.hasRemaining()) {
client.write(buffer); // echo back
}
// To shut down client: send "quit"
if ("quit".equalsIgnoreCase(received)) {
key.cancel();
client.close();
}
} else if (key.isWritable()) {
// Channel is ready to write (use only when you have data pending to write)
// Register OP_WRITE only when you have data to send, unregister when done
// to avoid tight selector loop when there is nothing to write
System.out.println("Channel writable: " + key.channel());
}
// If server should stop:
// serverChannel.close();
// selector.close();
// break;
}
}
}
}Step 7 — Network Basics: Socket and HttpClient
For traditional blocking network I/O, java.net.Socket is the foundation. For HTTP specifically, the modern java.net.http.HttpClient (Java 11+) replaces the old HttpURLConnection with a clean async API that supports HTTP/2 and WebSocket.
import java.io.*;
import java.net.*;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
public class NetworkExamples {
// ============================================================
// TCP Echo Server using blocking java.net
// ============================================================
static void startEchoServer(int port) {
new Thread(() -> {
try (ServerSocket server = new ServerSocket(port)) {
System.out.println("Echo server listening on " + port);
while (true) {
Socket client = server.accept(); // blocks for each new connection
// Spawn a thread per client (not scalable but simple)
new Thread(() -> handleClient(client)).start();
}
} catch (IOException e) {
System.out.println("Server stopped: " + e.getMessage());
}
}, "echo-server").start();
}
static void handleClient(Socket client) {
try (client;
BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(client.getOutputStream(), StandardCharsets.UTF_8), true)) {
System.out.println("Client connected: " + client.getInetAddress());
String line;
while ((line = in.readLine()) != null) {
System.out.println("Server got: " + line);
out.println("ECHO: " + line); // auto-flushed (PrintWriter with autoFlush=true)
}
} catch (IOException e) {
System.out.println("Client error: " + e.getMessage());
}
}
// ============================================================
// TCP Client
// ============================================================
static void runClient(int port) throws IOException {
try (Socket socket = new Socket("localhost", port);
PrintWriter out = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))) {
out.println("Hello from client");
System.out.println("Client received: " + in.readLine()); // ECHO: Hello from client
}
}
// ============================================================
// Modern HTTP client (Java 11+)
// ============================================================
static void httpClientExamples() throws Exception {
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // prefer HTTP/2
.connectTimeout(Duration.ofSeconds(10)) // connection timeout
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
// --- Synchronous GET ---
HttpRequest getRequest = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get"))
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(30))
.GET()
.build();
HttpResponse<String> response = client.send(getRequest,
HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
System.out.println("Headers: " + response.headers().map());
System.out.println("Body (first 100): " + response.body().substring(0, 100));
// --- Synchronous POST with JSON body ---
String jsonBody = "{\"name\":\"Alice\",\"age\":30}";
HttpRequest postRequest = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/post"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> postResponse = client.send(postRequest,
HttpResponse.BodyHandlers.ofString());
System.out.println("POST status: " + postResponse.statusCode());
// --- Asynchronous GET (non-blocking) ---
CompletableFuture<HttpResponse<String>> asyncResponse =
client.sendAsync(getRequest, HttpResponse.BodyHandlers.ofString());
asyncResponse
.thenApply(HttpResponse::body)
.thenAccept(body -> System.out.println("Async body length: " + body.length()))
.exceptionally(ex -> {
System.err.println("Request failed: " + ex.getMessage());
return null;
});
asyncResponse.get(); // wait for demo — in production, chain further
}
public static void main(String[] args) throws Exception {
startEchoServer(9090);
Thread.sleep(200);
runClient(9090);
httpClientExamples();
}
}Step 8 — Compression and Zip Archives
Java has built-in support for GZIP compression and ZIP archives through java.util.zip. These are decorator streams that wrap other streams, so they compose naturally with all the buffered and file I/O patterns seen earlier.
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.zip.*;
public class CompressionExample {
// ============================================================
// GZIP: compress and decompress a single file/stream
// ============================================================
static void gzipWrite(String text, Path gzFile) throws IOException {
try (GZIPOutputStream gzip = new GZIPOutputStream(
new BufferedOutputStream(Files.newOutputStream(gzFile)))) {
gzip.write(text.getBytes(StandardCharsets.UTF_8));
}
System.out.println("GZIP file size: " + Files.size(gzFile) + " bytes");
}
static String gzipRead(Path gzFile) throws IOException {
try (GZIPInputStream gzip = new GZIPInputStream(
new BufferedInputStream(Files.newInputStream(gzFile)))) {
return new String(gzip.readAllBytes(), StandardCharsets.UTF_8);
}
}
// ============================================================
// ZIP: multiple files in one archive
// ============================================================
static void createZip(Path zipFile, Path... filesToZip) throws IOException {
try (ZipOutputStream zos = new ZipOutputStream(
new BufferedOutputStream(Files.newOutputStream(zipFile)))) {
zos.setLevel(Deflater.BEST_COMPRESSION); // 0-9, default=6
for (Path file : filesToZip) {
// ZipEntry defines the name inside the archive
ZipEntry entry = new ZipEntry(file.getFileName().toString());
zos.putNextEntry(entry);
Files.copy(file, zos); // stream file contents into ZIP
zos.closeEntry();
}
}
System.out.println("ZIP created: " + zipFile + " (" + Files.size(zipFile) + " bytes)");
}
static void extractZip(Path zipFile, Path outputDir) throws IOException {
Files.createDirectories(outputDir);
try (ZipInputStream zis = new ZipInputStream(
new BufferedInputStream(Files.newInputStream(zipFile)))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
Path outPath = outputDir.resolve(entry.getName());
// ZIP SLIP protection: prevent path traversal attacks
if (!outPath.normalize().startsWith(outputDir.normalize())) {
throw new SecurityException("ZIP entry outside output dir: " + entry.getName());
}
if (entry.isDirectory()) {
Files.createDirectories(outPath);
} else {
Files.createDirectories(outPath.getParent());
Files.copy(zis, outPath, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Extracted: " + outPath);
}
zis.closeEntry();
}
}
}
// ============================================================
// Modern approach: ZipFile API for random access
// ============================================================
static void readSpecificEntry(Path zipFile, String entryName) throws IOException {
try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(zipFile.toFile())) {
ZipEntry entry = zf.getEntry(entryName);
if (entry != null) {
try (InputStream is = zf.getInputStream(entry)) {
System.out.println("Entry '" + entryName + "': " +
new String(is.readAllBytes(), StandardCharsets.UTF_8));
}
} else {
System.out.println("Entry not found: " + entryName);
}
}
}
public static void main(String[] args) throws IOException {
// GZIP demo
String data = "Hello compression! ".repeat(100);
Path gz = Path.of("data.gz");
System.out.println("Original size: " + data.length() + " bytes");
gzipWrite(data, gz);
System.out.println("Decompressed: " + gzipRead(gz).length() + " chars");
// ZIP demo
Path f1 = Path.of("file1.txt");
Path f2 = Path.of("file2.txt");
Files.writeString(f1, "Content of file 1");
Files.writeString(f2, "Content of file 2 with more data".repeat(10));
Path zip = Path.of("archive.zip");
createZip(zip, f1, f2);
extractZip(zip, Path.of("extracted"));
readSpecificEntry(zip, "file1.txt");
// Cleanup
Files.deleteIfExists(gz); Files.deleteIfExists(f1);
Files.deleteIfExists(f2); Files.deleteIfExists(zip);
Files.walk(Path.of("extracted"))
.sorted(java.util.Comparator.reverseOrder())
.forEach(p -> { try { Files.delete(p); } catch (IOException e) { } });
}
}