File Handling and I/O

A complete guide to Java file I/O: the File class, streams and readers/writers, buffering, try-with-resources, serialization, the NIO.2 API, RandomAccessFile, and working with CSV data.

The Two Worlds of Java File I/O

Java has two generations of file handling APIs. The original java.io package has been present since Java 1.0. It works well and is still widely used, but it has limitations: the File class does not distinguish cleanly between files and directories, methods that should throw an exception on failure often just return false, and there is no built-in support for symbolic links or file attributes.

Java 7 introduced the NIO.2 API in java.nio.file to address those gaps. The Path interface replaces File, and the Files utility class provides static methods that throw meaningful IOException subclasses rather than silently returning false. For new code, NIO.2 is the preferred choice. The legacy java.io classes still appear everywhere in existing codebases, so understanding both is necessary.

The java.io stream hierarchy

Java I/O is built from composable layers. Understanding the four base types helps you choose the right combination for any task.

  • InputStream / OutputStream:Abstract base classes for reading and writing raw bytes. Use for binary data: images, compressed files, serialized objects.
  • Reader / Writer:Abstract base classes for reading and writing characters with charset encoding. Use for text data.
  • Buffered wrappers:BufferedReader, BufferedWriter, BufferedInputStream, BufferedOutputStream. Wrap any Reader/Writer/Stream to reduce system calls by reading or writing in chunks.
  • Concrete sources:FileInputStream, FileOutputStream, FileReader, FileWriter. Connect the byte or character stream to an actual file on disk.

The File Class (java.io.File)

java.io.File represents a path in the file system. Despite its name, it can point to a file, a directory, or a path that does not yet exist. Creating a File object does not create anything on disk. It is merely a reference. The actual disk operations happen when you call methods on it.

The class provides inspection methods for checking properties, creation methods for making files and directories, and modification methods for renaming and deleting. One historical oddity to watch for: many methods return a boolean to indicate success or failure rather than throwing an exception. Forgetting to check these return values is a common source of silent bugs.

File Class: Creating, Inspecting, Renaming, Deleting

Java

Creating files and directories, checking properties, and performing basic file operations in the system temp directory.

FileInputStream, FileOutputStream, and try-with-resources

FileInputStream and FileOutputStream are the lowest-level file I/O classes. They work with raw bytes, making them the right choice for binary data: images, audio files, compiled bytecode, or anything that is not plain text. Reading one byte at a time is slow, so in practice you almost always wrap them in a buffered stream, but understanding the unbuffered classes helps you see what the higher-level abstractions are actually doing.

File streams hold an operating system file handle, which is a limited resource. Failing to close a stream leaks that handle. The traditional approach used a try-finally block with an explicit close in the finally clause. Java 7 introduced try-with-resources: place any AutoCloseable resource in the parentheses after the try keyword, and the compiler guarantees that close() is called when the block exits, whether normally or due to an exception. All stream and reader/writer classes implement AutoCloseable, so you should always use try-with-resources for file I/O. Multiple resources can be declared in the same try statement, separated by semicolons, and they are closed in reverse declaration order.

FileInputStream, FileOutputStream, and try-with-resources

Java

Writing bytes to a file and reading them back, with resource cleanup guaranteed by try-with-resources.

FileReader, FileWriter, BufferedReader, and BufferedWriter

For text files, the char-based Reader and Writer family handles the byte-to-character conversion for you using the platform's default charset. FileReader and FileWriter open a text file for reading and writing respectively. On their own they read and write one character at a time, which triggers a system call for each character. That is unacceptably slow for any non-trivial file.

Wrapping them in BufferedReader and BufferedWriter changes the pattern completely. The buffered wrapper reads a large block of characters from the underlying reader into memory in a single system call. Subsequent reads come from that in-memory buffer until it is empty, at which point another block is read. The result is a dramatic reduction in disk activity. BufferedReader also adds the critically useful readLine() method, which returns one complete line of text at a time and handles all three line ending conventions (CR, LF, and CRLF) transparently.

One important detail with FileWriter: by default, opening a FileWriter on an existing file truncates it. To append instead of overwrite, pass true as the second constructor argument: new FileWriter(file, true). This is easy to miss and produces frustrating data-loss bugs when overlooked.

Writing and Reading Text Files

Java

Writing multi-line text with BufferedWriter, reading it back line by line, and appending to an existing file.

PrintWriter

PrintWriter wraps any Writer or OutputStream and adds print(), println(), and printf() methods familiar from console output. It is the simplest way to write formatted text to a file. Unlike most I/O classes, PrintWriter swallows exceptions by default and sets an internal error flag instead. Always check checkError() after writing, or wrap it around a BufferedWriter that does throw exceptions.

PrintWriter

Java

Using printf-style formatting to write a structured report to a file.

Serialization and Deserialization

Serialization is the process of converting a live Java object into a sequence of bytes that can be written to a file or sent over a network. Deserialization is the reverse: reading those bytes back and reconstructing the original object. The mechanism is built into the Java runtime and requires minimal code when all the types involved are serializable.

For a class to be serializable, it must implement the java.io.Serializable interface. This is a marker interface: it has no methods. Its presence simply signals to the serialization mechanism that objects of this class are allowed to be serialized. Every non-static, non-transient field of the class must itself be serializable, or serialization will throw a NotSerializableException at runtime.

The serialVersionUID field deserves explicit attention. It is a long that uniquely identifies a version of the class. When deserializing, Java compares the UID in the stream with the UID of the loaded class. If they do not match, a InvalidClassException is thrown. If you do not declare it, the JVM computes it automatically from the class's structure. That automatic value changes whenever you add or remove a field, which breaks deserialization of previously serialized data. Declaring it explicitly gives you control over when the version number changes.

The transient keyword

Fields that should not be serialized are marked transient. Common reasons to use it:

  • Sensitive data:Passwords, private keys, or tokens that must not be written to disk or sent over the network.
  • Non-serializable fields:References to non-serializable objects like database connections or thread handles, which have no meaningful serialized form.
  • Derived fields:Values that can be recomputed from other fields after deserialization. No need to waste space storing them.
  • After deserialization:A transient field is initialized to its default: null for objects, 0 for numbers, false for booleans. You can restore it by implementing readObject().

Serialization and Deserialization

Java

Serializing a User object to a file, reading it back, and demonstrating how transient fields are handled.

NIO.2: Path, Paths, and the Files Class

The NIO.2 API is the modern way to work with the file system in Java. The Path interface represents a file system path in an immutable, type-safe way. The Paths class (or Path.of() in Java 11+) creates Path instances from strings or URIs. The Files utility class provides static methods for all common operations: reading, writing, copying, moving, deleting, creating, and walking the directory tree. Unlike the oldFile class, these methods throw descriptive exceptions like NoSuchFileException, FileAlreadyExistsException, and AccessDeniedException when things go wrong, which makes debugging far easier.

Path, Paths, and Files

Java

Creating paths, navigating the path hierarchy, and common Files operations for creation, reading, and writing.

Files.walk() and Files.list()

Files.walk() returns a Stream<Path> that lazily traverses a directory tree in depth-first order. Because it returns a stream, you can apply any of the standard stream operations: filter by extension, collect file names, compute sizes, or find specific files. The stream holds a directory handle open while it is being consumed, so you must close it when done. Always use it in a try-with-resources block.

Files.list() is the non-recursive equivalent: it returns a stream of only the direct children of a directory, without descending into subdirectories. Use it when you only need one level of a directory and want to avoid the overhead of a full tree walk.

Files.walk() and Files.list()

Java

Walking a directory tree to find files by extension and reading file attributes.

RandomAccessFile

Sequential streams read from start to finish, or write from start to end. That works for most file processing tasks, but occasionally you genuinely need to jump to a specific position in a file, read something, then jump somewhere else. RandomAccessFile provides exactly that capability through its seek(long) method, which moves the internal file pointer to any byte offset.

You open a RandomAccessFile with a mode string: "r" for read-only, or "rw" for read and write. The class supports reading and writing all primitive types directly with methods like writeInt(), readDouble(), and writeUTF(). Real-world use cases include fixed-size record databases, binary index files, and log files where you read the tail efficiently without loading the entire file.

RandomAccessFile

Java

Writing fixed-size records and then reading back specific records by computing their byte offset.

Working with CSV Manually

CSV (Comma-Separated Values) is the most common format for exchanging tabular data. While production applications typically use a library like OpenCSV or Apache Commons CSV for robustness, understanding how to handle CSV manually sharpens your file I/O skills and is practical for simple data files where adding a dependency is not warranted.

The naive approach is to split each line on commas. That breaks as soon as a field value contains a comma, which is common for addresses, names, and descriptions. The CSV specification handles this by wrapping such fields in double quotes: "New York, NY". A field that contains a double quote character escapes it as two consecutive double quotes: "He said ""hello""". A correct manual parser tracks whether the current character is inside a quoted field before deciding whether a comma is a delimiter or part of the value.

Writing and Reading CSV Manually

Java

Writing a CSV file with proper quoting and reading it back with a parser that handles quoted fields containing commas.

Quiz - Test Your Knowledge

Ten questions covering the java.io.File class, stream types and buffering, try-with-resources, serialization and the transient keyword, NIO.2 Path and Files operations, Files.walk(), file attributes, RandomAccessFile, and CSV parsing. Read each option carefully before selecting your answer.

Knowledge Check

1. What is the primary advantage of try-with-resources over a traditional try-finally block for file handling?

2. What is the key difference between FileReader and BufferedReader?

3. What does the transient keyword do to a field in a serializable class?

4. What is the main advantage of java.nio.file.Path over java.io.File?

5. What does Files.walk() do?

6. For a class to be serializable, what must it do?

7. What is the purpose of serialVersionUID in a Serializable class?

8. What does RandomAccessFile allow that sequential streams do not?

9. When manually parsing a CSV file, what is the correct way to handle a field that contains a comma inside double quotes, such as "New York, NY"?

10. Which Files method is the simplest way to read all lines of a UTF-8 text file into a List<String> in one call?