Skip to content

Java Files and I/O

Input/Output (I/O) operations are essential for any application that needs to read data from a source (like a file, network connection, or keyboard) or write data to a destination (like a file, network, or console).

Java provides a rich set of APIs for handling I/O, primarily located in the java.io and java.nio (New I/O) packages.

In Java I/O, a stream represents a sequence of data flowing from a source to a destination. Think of it like a pipe carrying data.

  • Input Streams: Read data from a source (e.g., InputStream, Reader).
  • Output Streams: Write data to a destination (e.g., OutputStream, Writer).

Streams are categorized based on the type of data they handle:

  • Byte Streams: Handle raw binary data (8-bit bytes). Suitable for images, audio, or any non-textual data. Core classes: InputStream, OutputStream.
  • Character Streams: Handle character data (16-bit Unicode characters). Automatically handle character encoding conversions. Ideal for text files. Core classes: Reader, Writer.

Modern File I/O: The java.nio.file Package (NIO.2)

Section titled “Modern File I/O: The java.nio.file Package (NIO.2)”

Introduced in Java 7, the java.nio.file package (often called NIO.2) provides a more modern, flexible, and powerful API for interacting with the file system compared to the older java.io.File class. It’s generally the recommended approach for file operations.

  • Path: Represents the path to a file or directory in the file system. It’s operating system independent.
  • Paths: A factory class with static methods (like get()) to create Path objects from strings or URIs.
  • Files: A utility class with numerous static methods for common file operations (reading, writing, copying, deleting, checking existence, managing attributes, etc.).
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathExample {
public static void main(String[] args) {
// Create a Path object (OS-independent way)
Path filePath = Paths.get("data", "input.txt"); // Relative path
Path configPath = Paths.get("/etc/app/config.properties"); // Absolute path (example)
Path userHome = Paths.get(System.getProperty("user.home"), "documents", "report.pdf");
System.out.println("File Path: " + filePath);
System.out.println("File Name: " + filePath.getFileName());
System.out.println("Parent Directory: " + filePath.getParent());
System.out.println("Absolute Path: " + filePath.toAbsolutePath());
}
}

Reading Small Files (All Lines into Memory):

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.List;
import java.nio.charset.StandardCharsets;
public class ReadAllLines {
public static void main(String[] args) {
Path filePath = Paths.get("input.txt"); // Assume input.txt exists
try {
List<String> lines = Files.readAllLines(filePath, StandardCharsets.UTF_8);
System.out.println("File Content:");
for (String line : lines) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
// e.printStackTrace(); // For detailed stack trace
}
}
}

Reading Large Files (Using Streams and try-with-resources): For large files, reading line by line is more memory-efficient.

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class ReadLineByLine {
public static void main(String[] args) {
Path filePath = Paths.get("large_input.txt");
// try-with-resources ensures the reader is closed automatically
try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) {
String line;
System.out.println("Reading line by line:");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
}
}

Writing a String or List of Strings:

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.nio.charset.StandardCharsets;
import java.nio.file.StandardOpenOption;
public class WriteToFile {
public static void main(String[] args) {
Path outputPath = Paths.get("output.txt");
List<String> linesToWrite = Arrays.asList("First line", "Second line", "End of file.");
try {
// Write lines (overwrites existing file by default)
Files.write(outputPath, linesToWrite, StandardCharsets.UTF_8);
System.out.println("Successfully wrote lines to " + outputPath);
// Append lines to the file
Files.write(outputPath, Arrays.asList("Appended line."), StandardCharsets.UTF_8,
StandardOpenOption.APPEND);
System.out.println("Successfully appended to " + outputPath);
} catch (IOException e) {
System.err.println("Error writing to file: " + e.getMessage());
}
}
}

Writing Large Amounts of Data (Using Streams and try-with-resources):

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class WriteWithStream {
public static void main(String[] args) {
Path filePath = Paths.get("stream_output.txt");
try (BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8)) {
for (int i = 1; i <= 10; i++) {
writer.write("Line number " + i);
writer.newLine(); // Write a platform-specific line separator
}
System.out.println("Successfully wrote using stream to " + filePath);
} catch (IOException e) {
System.err.println("Error writing file: " + e.getMessage());
}
}
}

A critical aspect of I/O is closing resources (like streams) when you’re finished with them to release system resources and prevent leaks. Failing to close streams can lead to problems.

The try-with-resources statement (introduced in Java 7) automates resource management. Any resource declared within the parentheses of the try statement (which must implement java.lang.AutoCloseable, like most I/O streams) will be automatically closed at the end of the try block, whether it completes normally or throws an exception.

This is the standard and highly recommended way to handle I/O resources. Avoid manual close() calls in finally blocks unless necessary for pre-Java 7 code.

try (ResourceType resource1 = initializeResource1();
ResourceType resource2 = initializeResource2()) {
// Use resource1 and resource2
} catch (IOException e) {
// Handle exceptions
}
// resources are automatically closed here

The Files class offers many useful methods:

import java.nio.file.*;
import java.io.IOException;
public class FileOperations {
public static void main(String[] args) throws IOException { // Simple example, real code needs try-catch
Path source = Paths.get("input.txt");
Path destination = Paths.get("backup", "input_copy.txt");
Path newDir = Paths.get("data", "subdir");
Path fileToCheck = Paths.get("output.txt");
// Check Existence
boolean exists = Files.exists(fileToCheck);
System.out.println(fileToCheck + " exists: " + exists);
System.out.println(fileToCheck + " does not exist: " + Files.notExists(fileToCheck));
System.out.println(fileToCheck + " is readable: " + Files.isReadable(fileToCheck));
// Create Directories (mkdirs equivalent)
if (Files.notExists(newDir.getParent())) {
Files.createDirectories(newDir.getParent()); // Create parent directories if needed
}
if (Files.notExists(newDir)) {
Files.createDirectory(newDir);
System.out.println("Created directory: " + newDir);
}
// Copy File
if (Files.exists(source) && Files.exists(destination.getParent())) {
Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Copied " + source + " to " + destination);
}
// Move (Rename) File
// Path target = Paths.get("data", "renamed_input.txt");
// Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
// System.out.println("Moved " + source + " to " + target);
// Delete File
// if (Files.exists(destination)) {
// Files.delete(destination);
// System.out.println("Deleted: " + destination);
// }
// List Directory Contents
Path dirToList = Paths.get("."); // Current directory
System.out.println("\nListing contents of: " + dirToList.toAbsolutePath());
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dirToList)) {
for (Path entry : stream) {
System.out.println(entry.getFileName());
}
} catch (IOException e) {
System.err.println("Error listing directory: " + e);
}
}
}

Java provides three standard streams accessible via the System class:

  • System.in: Standard Input Stream (usually keyboard input). An InputStream.
  • System.out: Standard Output Stream (usually console output). A PrintStream.
  • System.err: Standard Error Stream (usually console output, for error messages). A PrintStream.

The easiest way to read formatted input (like numbers, words, lines) from the console is using the java.util.Scanner class.

import java.util.Scanner;
public class ReadConsole {
public static void main(String[] args) {
// Use try-with-resources for the Scanner
try (Scanner scanner = new Scanner(System.in)) {
System.out.print("Enter your name: ");
String name = scanner.nextLine(); // Read a full line
System.out.print("Enter your age: ");
int age = scanner.nextInt(); // Read an integer
System.out.println("\nHello, " + name + "! You are " + age + " years old.");
} catch (java.util.InputMismatchException e) {
System.err.println("Invalid input. Please enter a number for age.");
}
// Scanner is automatically closed here
}
}

Writing to Standard Output/Error (System.out, System.err)

Section titled “Writing to Standard Output/Error (System.out, System.err)”

You’ve already seen System.out.println() extensively. System.err works the same way but is conventionally used for error messages.

public class StandardOutput {
public static void main(String[] args) {
System.out.println("This is standard output.");
System.err.println("This is an error message.");
int value = 10;
System.out.printf("The value is %d%n", value); // Formatted output
}
}

While java.nio.file is preferred for new code, you might encounter older code using classes like java.io.File, FileInputStream, FileOutputStream, FileReader, and FileWriter. It’s useful to recognize them.

java.io.File: Represents a file or directory path but has limitations compared to java.nio.file.Path. Many of its methods don’t throw exceptions on failure, making error handling less robust.

FileInputStream/FileOutputStream: Used for reading/writing raw bytes.

FileReader/FileWriter: Used for reading/writing characters, using the platform’s default character encoding (which can be problematic). It’s generally better to wrap byte streams with InputStreamReader/OutputStreamWriter and explicitly specify the encoding (e.g., StandardCharsets.UTF_8).

Example (Legacy Copy with try-with-resources):

import java.io.*;
public class LegacyCopyFile {
public static void main(String[] args) {
File sourceFile = new File("input.txt");
File destFile = new File("output_legacy.txt");
// Still use try-with-resources!
try (InputStream in = new FileInputStream(sourceFile);
OutputStream out = new FileOutputStream(destFile)) {
byte[] buffer = new byte[1024]; // Read/write in chunks
int lengthRead;
while ((lengthRead = in.read(buffer)) > 0) {
out.write(buffer, 0, lengthRead);
}
System.out.println("File copied successfully using legacy IO.");
} catch (IOException e) {
System.err.println("Error copying file: " + e.getMessage());
}
}
}

Always prefer java.nio.file and try-with-resources for modern Java I/O operations.

Understanding how to handle potential errors during I/O or other operations is crucial. The next chapter covers Java Exceptions, explaining how to anticipate, catch, and handle errors gracefully in your programs.