🃏 Java I/O - File Reading and Writing
Rule: Java I/O provides multiple ways to read/write files with different performance characteristics.
- Files.readString()/writeString(): Simple text file operations (Java 11+)
- BufferedReader/Writer: Efficient line-by-line processing
- FileInputStream/OutputStream: Byte-level operations
import java.nio.file.*;
import java.io.*;
import java.util.List;
// Simple file operations (Java 11+)
Path textFile = Path.of("data.txt");
// Write string to file
String content = "Hello\nWorld\nJava";
Files.writeString(textFile, content);
// Read entire file as string
String fileContent = Files.readString(textFile);
System.out.println(fileContent);
// Read all lines into List
List<String> lines = Files.readAllLines(textFile);
lines.forEach(System.out::println);
// Write lines to file
List<String> outputLines = List.of("Line 1", "Line 2", "Line 3");
Files.write(textFile, outputLines);
Buffered I/O for large files:
// Efficient reading with BufferedReader
try (BufferedReader reader = Files.newBufferedReader(Path.of("large.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
// Efficient writing with BufferedWriter
try (BufferedWriter writer = Files.newBufferedWriter(Path.of("output.txt"))) {
writer.write("First line");
writer.newLine();
writer.write("Second line");
writer.newLine();
}
// Stream processing for very large files
try (Stream<String> lines = Files.lines(Path.of("huge.txt"))) {
lines.filter(line -> line.contains("important"))
.map(String::toUpperCase)
.forEach(System.out::println);
}
Byte-level operations:
// Copy file using byte arrays
try (FileInputStream in = new FileInputStream("source.dat");
FileOutputStream out = new FileOutputStream("dest.dat")) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
// Files utility for copying
Files.copy(Path.of("source.txt"), Path.of("destination.txt"),
StandardCopyOption.REPLACE_EXISTING);
💡 Learning Tip: Remember “FILES = SIMPLE, STREAMS = CONTROL” - Files class for simple operations, streams for fine-grained control and large files.
Q: When should you use Files.readString() vs BufferedReader?
A: Use Files.readString() for small files when you need the entire content. Use BufferedReader for large files or when processing line-by-line to avoid memory issues.
🃏 Files.mismatch() and Path Operations
Files.mismatch() - Compares two files byte by byte:
- Returns index of first mismatching byte (0-based)
- Returns -1 if files are identical
- Throws
IOException
if paths are invalid or inaccessible
import java.nio.file.*;
import java.io.IOException;
try {
Path file1 = Path.of("document1.txt"); // Content: "Hello World"
Path file2 = Path.of("document2.txt"); // Content: "Hello Mars"
Path file3 = Path.of("document3.txt"); // Content: "Hello World"
long result1 = Files.mismatch(file1, file2); // Returns 6 (index of 'W' vs 'M')
long result2 = Files.mismatch(file1, file3); // Returns -1 (identical)
System.out.println("Mismatch at byte: " + result1); // 6
System.out.println("Files identical: " + (result2 == -1)); // true
} catch (IOException e) {
System.out.println("Error reading files: " + e.getMessage());
}
Other useful Path/Files operations (Java 21):
// Path operations:
Path path = Path.of("users", "documents", "file.txt");
Path absolute = path.toAbsolutePath();
Path parent = path.getParent();
Path filename = path.getFileName();
// Files operations:
boolean exists = Files.exists(path);
boolean readable = Files.isReadable(path);
long size = Files.size(path);
String content = Files.readString(path);
List<String> lines = Files.readAllLines(path);
// Directory operations:
Files.createDirectories(Path.of("new/nested/directory"));
try (var stream = Files.walk(Path.of("."))) {
stream.filter(Files::isRegularFile)
.forEach(System.out::println);
}
💡 Learning Tip: Mismatch = “Find the first difference” (-1 means no differences found).
🃏 Path Operations and Resolution
Rule: Path provides methods for manipulating file system paths without accessing the actual file system.
- Path operations are purely textual - they don’t check if files exist
- Path.resolve() combines paths intelligently
- Path.relativize() finds relative path between two paths
import java.nio.file.Path;
// Path creation
Path path1 = Path.of("home", "user", "documents"); // home/user/documents
Path path2 = Path.of("/usr/local/bin"); // /usr/local/bin
Path path3 = Path.of("C:", "Users", "John", "file.txt"); // C:\Users\John\file.txt (Windows)
// Path components
Path fullPath = Path.of("/home/user/documents/file.txt");
System.out.println("Root: " + fullPath.getRoot()); // /
System.out.println("Parent: " + fullPath.getParent()); // /home/user/documents
System.out.println("Filename: " + fullPath.getFileName()); // file.txt
System.out.println("Name count: " + fullPath.getNameCount()); // 4
System.out.println("Name(1): " + fullPath.getName(1)); // user
// Path resolution
Path base = Path.of("/home/user");
Path relative = Path.of("documents/file.txt");
Path absolute = Path.of("/etc/config");
Path resolved1 = base.resolve(relative); // /home/user/documents/file.txt
Path resolved2 = base.resolve(absolute); // /etc/config (absolute path wins)
Path resolved3 = base.resolve("temp.txt"); // /home/user/temp.txt
Path normalization and relativization:
// Normalize - removes redundant elements
Path messy = Path.of("/home/user/../user/./documents/../documents/file.txt");
Path clean = messy.normalize(); // /home/user/documents/file.txt
// Relativize - find relative path between two paths
Path from = Path.of("/home/user/documents");
Path to = Path.of("/home/user/pictures/vacation.jpg");
Path relative = from.relativize(to); // ../pictures/vacation.jpg
Path from2 = Path.of("/home/user");
Path to2 = Path.of("/home/user/documents/file.txt");
Path relative2 = from2.relativize(to2); // documents/file.txt
// Absolute paths
Path abs1 = Path.of("/home/user/docs");
Path abs2 = Path.of("/var/log/app.log");
Path relative3 = abs1.relativize(abs2); // ../../../var/log/app.log
Path comparison and testing:
Path path1 = Path.of("documents/file.txt");
Path path2 = Path.of("documents", "file.txt");
Path path3 = Path.of("DOCUMENTS/FILE.TXT");
System.out.println(path1.equals(path2)); // true (same path)
System.out.println(path1.equals(path3)); // false (case sensitive on Unix)
// Check path characteristics (textual, not file system)
System.out.println(path1.isAbsolute()); // false
System.out.println(Path.of("/home").isAbsolute()); // true
// startsWith/endsWith work on path elements, not strings
Path fullPath = Path.of("/home/user/documents/file.txt");
System.out.println(fullPath.startsWith("/home")); // true
System.out.println(fullPath.startsWith("/ho")); // false (not complete element)
System.out.println(fullPath.endsWith("file.txt")); // true
System.out.println(fullPath.endsWith("uments/file.txt")); // false (not complete elements)
💡 Learning Tip: Think “PATH = GPS DIRECTIONS” - Path operations calculate routes and relationships between locations without checking if the locations actually exist.
Q: What’s the difference between path1.resolve(path2)
when path2 is relative vs absolute?
A: If path2 is relative, it’s appended to path1. If path2 is absolute, path2 is returned unchanged (absolute paths “win”).
🃏 Stream I/O Classes Hierarchy
Rule: Java I/O uses decorator pattern with byte streams (InputStream/OutputStream) and character streams (Reader/Writer).
- Byte streams: Handle raw binary data (images, videos, executables)
- Character streams: Handle text data with encoding support
- Buffered streams: Add buffering for performance
- Bridge streams: Convert between byte and character streams
import java.io.*;
// Byte streams hierarchy
// InputStream <- FileInputStream, BufferedInputStream, ObjectInputStream
// OutputStream <- FileOutputStream, BufferedOutputStream, ObjectOutputStream
// Reading bytes
try (FileInputStream fis = new FileInputStream("data.bin");
BufferedInputStream bis = new BufferedInputStream(fis)) {
int byteValue;
while ((byteValue = bis.read()) != -1) {
// Process byte (0-255 or -1 for EOF)
System.out.print(byteValue + " ");
}
}
// Writing bytes
try (FileOutputStream fos = new FileOutputStream("output.bin");
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
byte[] data = {65, 66, 67, 68, 69}; // ASCII for ABCDE
bos.write(data);
bos.flush(); // Ensure data is written
}
// Character streams hierarchy
// Reader <- FileReader, BufferedReader, InputStreamReader
// Writer <- FileWriter, BufferedWriter, OutputStreamWriter
// Reading characters
try (FileReader fr = new FileReader("text.txt");
BufferedReader br = new BufferedReader(fr)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
// Writing characters
try (FileWriter fw = new FileWriter("output.txt");
BufferedWriter bw = new BufferedWriter(fw)) {
bw.write("Hello World");
bw.newLine();
bw.write("Java I/O");
bw.flush();
}
Bridge streams - converting between byte and character:
// InputStreamReader - byte stream to character stream
try (FileInputStream fis = new FileInputStream("input.txt");
InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr)) {
String line = br.readLine();
System.out.println(line);
}
// OutputStreamWriter - character stream to byte stream
try (FileOutputStream fos = new FileOutputStream("output.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw)) {
bw.write("Unicode text: 你好");
bw.flush();
}
// System streams
BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
PrintWriter output = new PrintWriter(System.out);
String userInput = console.readLine();
output.println("You entered: " + userInput);
output.flush();
Object serialization:
// Serializable class
class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
// Write object
try (FileOutputStream fos = new FileOutputStream("person.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
Person person = new Person("Alice", 30);
oos.writeObject(person);
}
// Read object
try (FileInputStream fis = new FileInputStream("person.ser");
ObjectInputStream ois = new ObjectInputStream(fis)) {
Person person = (Person) ois.readObject();
System.out.println(person); // Person{name='Alice', age=30}
}
Performance considerations:
// ❌ Slow - unbuffered I/O
try (FileReader fr = new FileReader("largefile.txt")) {
int ch;
while ((ch = fr.read()) != -1) { // Each read() is a system call
System.out.print((char) ch);
}
}
// ✅ Fast - buffered I/O
try (FileReader fr = new FileReader("largefile.txt");
BufferedReader br = new BufferedReader(fr)) {
int ch;
while ((ch = br.read()) != -1) { // Reads from internal buffer
System.out.print((char) ch);
}
}
// ✅ Fastest - read lines at once
try (FileReader fr = new FileReader("largefile.txt");
BufferedReader br = new BufferedReader(fr)) {
String line;
while ((line = br.readLine()) != null) { // Read entire lines
System.out.println(line);
}
}
💡 Learning Tip: Remember “BYTE vs CHAR, BUFFER for SPEED” - use byte streams for binary data, character streams for text, and always add buffering for performance.
Q: When should you use InputStreamReader vs FileReader?
A: Use InputStreamReader when you need explicit encoding control or are wrapping a byte stream. FileReader is a convenience class that uses the platform default encoding.