Java I/O API
Introduction
This lesson covers the ninth objective of the Oracle Certified Professional Java SE 21 Developer exam (1Z0-830): Using Java I/O API. Oracle lists three skills: reading and writing console and file data with I/O streams, serializing and deserializing Java objects, and constructing, traversing, creating, reading and writing Path objects and their properties with the java.nio.file API. Oracle does not publish a weighting per objective. In practice, expect code snippets that call Path methods and ask for the printed value, snippets that copy or move files and ask which exception is thrown, and serialization snippets that ask which fields survive a round trip.
To keep the lesson concrete, we work on a small invoicing tool for a bookshop: it reads a CSV of orders, writes a report, stores invoice objects on disk, and archives old files. Every example below belongs to that tool.
Learning objectives
After this lesson you will be able to:
- Use byte streams (
InputStream,OutputStream) and character streams (Reader,Writer) with buffering, and choose between them. - Read from and write to the console with
System.in,System.outandjava.io.Console. - Read and write text and binary files with
Files,BufferedReader,BufferedWriter,PrintWriter. - Create, resolve, relativize, normalize and compare
Pathobjects. - Copy, move, delete, list and walk files and directories with
Files, with the rightStandardCopyOptionandOpenOptionvalues. - Read file attributes.
- Serialize and deserialize objects, and explain the effect of
Serializable,transient,staticandserialVersionUID.
1. Streams: bytes and characters
java.io has two families.
| Family | Base classes | Unit | Typical subclasses |
|---|---|---|---|
| Byte streams | InputStream, OutputStream |
int (0 to 255) / byte |
FileInputStream, FileOutputStream, ByteArrayInputStream, BufferedInputStream, DataInputStream, ObjectInputStream |
| Character streams | Reader, Writer |
char |
FileReader, FileWriter, BufferedReader, BufferedWriter, InputStreamReader, PrintWriter, StringReader |
InputStream.read() returns an int in the range 0 to 255, or -1 at end of stream. read(byte[]) returns the number of bytes read, or -1. Bridges between the families are InputStreamReader and OutputStreamWriter, which take a charset.
Wrappers add features (decorator pattern):
try (BufferedReader in = new BufferedReader(new FileReader("orders.csv"));
PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("report.txt")))) {
String line;
while ((line = in.readLine()) != null) { // null at end of file
out.println(line.toUpperCase());
}
} // closes both, in reverse order
Rules:
- Closing a wrapper closes the wrapped stream. With try-with-resources, resources close in reverse order of declaration.
BufferedWritercontent may not reach the file untilflush()orclose(). If you forget to close, data can be lost.FileWritertruncates the file by default;new FileWriter(name, true)appends.FileOutputStreambehaves the same way with its second argument.readLine()removes the line terminator and returnsnullat the end.PrintWriterandPrintStreamnever throwIOException; they set an internal error flag checked withcheckError().FileReaderandFileWriteruse the default charset (UTF-8 by default since Java 18, JEP 400). Always prefer an explicit charset:new FileReader(file, StandardCharsets.UTF_8).DataOutputStreamwrites primitives in binary (writeInt,writeUTF), andDataInputStreammust read them back in the same order.mark(int)/reset()work only whenmarkSupported()is true (for exampleBufferedReader,ByteArrayInputStream).
2. Console I/O
System.in is an InputStream, System.out and System.err are PrintStreams. A Scanner or BufferedReader makes keyboard input comfortable:
Scanner sc = new Scanner(System.in);
int qty = sc.nextInt();
sc.nextLine(); // consume the rest of the line
String note = sc.nextLine();
A classic bug: nextInt() does not consume the line terminator, so the next nextLine() returns an empty string.
java.io.Console is obtained with System.console(), which returns null when there is no interactive terminal (IDE, redirected input, many services). Its main methods are readLine(), readPassword() (returns a char[] and does not echo), printf and writer(). Always test for null. Note that Java 22 changed the behavior of System.console() for non-interactive cases, but on Java 21 the null rule applies.
3. Files with java.nio.file
The NIO.2 API (Java 7) is the modern way. Its core types are the interface Path, the utility class Files and the helper Paths (or Path.of, preferred since Java 11).
Creating and inspecting paths
Path p = Path.of("/shop/data/../invoices/2026/inv-001.txt");
p.getFileName(); // inv-001.txt
p.getParent(); // /shop/data/../invoices/2026
p.getRoot(); // /
p.getNameCount(); // 6 (shop, data, .., invoices, 2026, inv-001.txt)
p.getName(0); // shop
p.subpath(1, 3); // data/..
p.isAbsolute(); // true
p.normalize(); // /shop/invoices/2026/inv-001.txt
Path methods are purely syntactic: they do not check that the file exists. Files.exists(path) and Files.notExists(path) do. Note that both can be false (when access is denied or status is unknown).
resolve, relativize, normalize, toAbsolutePath
| Method | Behavior | Example result |
|---|---|---|
a.resolve(b) |
Appends b to a; if b is absolute, returns b |
/shop.resolve(inv/1.txt) = /shop/inv/1.txt |
a.resolveSibling(b) |
Resolves b against a.getParent() |
/shop/a.txt.resolveSibling(b.txt) = /shop/b.txt |
a.relativize(b) |
Path from a to b; both must be of the same kind (both absolute or both relative) else IllegalArgumentException |
/shop.relativize(/shop/inv/1.txt) = inv/1.txt |
p.normalize() |
Removes . and resolves .. lexically |
a/b/../c becomes a/c |
p.toAbsolutePath() |
Prepends the working directory if relative | |
p.toRealPath() |
Resolves symbolic links, checks existence, throws IOException if absent |
relativize with ..: Path.of("/a/b").relativize(Path.of("/a/c/d")) is ../c/d. Path.of("a").resolve("") returns a. normalize does not touch the file system, so on a symbolic link it can differ from the real parent.
Reading and writing whole files
Path orders = Path.of("data", "orders.csv");
List<String> lines = Files.readAllLines(orders); // UTF-8
String text = Files.readString(orders); // Java 11
byte[] bytes = Files.readAllBytes(orders);
try (Stream<String> s = Files.lines(orders)) { // lazy, must be closed
long big = s.filter(l -> l.endsWith(",PAID")).count();
}
Files.writeString(Path.of("report.txt"), "Total: 42\n");
Files.write(Path.of("report.txt"), List.of("a", "b"), StandardOpenOption.APPEND);
try (BufferedWriter w = Files.newBufferedWriter(Path.of("out.txt"))) {
w.write("hello");
w.newLine();
}
Files.write and writeString default to CREATE, TRUNCATE_EXISTING and WRITE. With APPEND, the file is extended. With CREATE_NEW, an existing file causes FileAlreadyExistsException. Opening a missing file for reading throws NoSuchFileException. All of these are subclasses of IOException.
Create, copy, move, delete
Files.createDirectory(Path.of("archive")); // parent must exist
Files.createDirectories(Path.of("archive/2026/10")); // creates all, no error if present
Files.createFile(Path.of("archive/readme.txt")); // FileAlreadyExistsException if present
Files.copy(src, dst); // fails if dst exists
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
Files.move(src, dst, StandardCopyOption.ATOMIC_MOVE);
Files.delete(path); // NoSuchFileException if absent, DirectoryNotEmptyException if non-empty directory
Files.deleteIfExists(path); // returns boolean
Important details:
Files.copyof a directory copies only the directory itself, not its content (an empty directory is created). Copy is shallow.Files.copydoes not copy file attributes unlessCOPY_ATTRIBUTESis passed.Files.moveof a non-empty directory within the same file store works as a rename.ATOMIC_MOVEcannot be combined with other options meaningfully, and the implementation may throwAtomicMoveNotSupportedException.- There is also
Files.copy(InputStream, Path)andFiles.copy(Path, OutputStream).
Listing and walking
| Method | Depth | Returns | Notes |
|---|---|---|---|
Files.list(dir) |
One level | Stream<Path> |
Must be closed |
Files.walk(dir) |
Recursive (all) | Stream<Path> |
Includes dir itself; walk(dir, maxDepth) limits depth |
Files.find(dir, depth, BiPredicate) |
Recursive | Stream<Path> |
Predicate receives path and attributes |
Files.newDirectoryStream(dir, glob) |
One level | DirectoryStream<Path> |
Glob filter, closeable |
Files.walkFileTree(dir, visitor) |
Recursive | Visitor callbacks, used for recursive delete |
try (Stream<Path> s = Files.walk(Path.of("archive"), 2)) {
s.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".txt"))
.forEach(System.out::println);
}
Walking depth: walk(dir, 1) is equivalent to list(dir) plus dir itself. A recursive delete of a tree needs walkFileTree or a reverse-sorted walk, because Files.delete refuses non-empty directories.
Attributes
long size = Files.size(path);
boolean dir = Files.isDirectory(path);
FileTime modified = Files.getLastModifiedTime(path);
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
attrs.creationTime(); attrs.isSymbolicLink();
Files.isSameFile(a, b); // compares the actual files, can throw IOException
Files.isReadable(path); Files.isWritable(path); Files.isHidden(path);
Path equality: Path.of("a").equals(Path.of("a/")) is true (trailing separator is removed), but Path.of("a/../b") is not equal to Path.of("b"), since equality is syntactic.
4. Legacy java.io.File
File still appears. new File("x") does nothing on disk. exists(), createNewFile(), mkdir(), mkdirs(), delete() return booleans instead of throwing, list() returns String[], and toPath() bridges to NIO. Path.toFile() goes the other way. New code should prefer Path.
5. Serialization
Serialization converts an object graph into a byte stream with ObjectOutputStream.writeObject; ObjectInputStream.readObject reconstructs it. The class must implement the marker interface java.io.Serializable, otherwise NotSerializableException is thrown at write time.
class Invoice implements Serializable {
private static final long serialVersionUID = 1L;
private String number;
private transient String internalNote; // not serialized
private static int counter = 99; // not serialized (belongs to the class)
private Customer customer; // Customer must be Serializable too
Invoice(String number, String note) { this.number = number; this.internalNote = note; }
}
try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(Path.of("inv.ser")))) {
out.writeObject(new Invoice("INV-1", "secret"));
}
try (ObjectInputStream in = new ObjectInputStream(Files.newInputStream(Path.of("inv.ser")))) {
Invoice copy = (Invoice) in.readObject(); // ClassNotFoundException, IOException
// copy.internalNote == null
}
Rules to memorize:
- Constructors: when deserializing, the constructor of the serializable class (and of serializable ancestors) is not called. The no-argument constructor of the first non-serializable superclass is called, so that class must have an accessible no-argument constructor, otherwise
InvalidClassExceptionoccurs at read time. - Default values:
transientfields come back as their default (null,0,false), not the initializer value from the declaration (initializers do not run either, since the constructor is skipped). - Static fields are not part of the object state.
- Object graph: every referenced object must be serializable or the field must be
transient; otherwiseNotSerializableException. - serialVersionUID: a
longconstant used to check compatibility. If absent, the JVM computes one from the class structure, so a small change makes old files unreadable withInvalidClassException. Declare it explicitly. - Inheritance: if the superclass is serializable, so are all subclasses. Fields of a non-serializable superclass are not saved.
- Shared references are preserved within one stream: if two fields point at the same object, they point to one copy after reading.
- Records can be serializable by implementing
Serializable; their state is rebuilt through the canonical constructor, andserialVersionUIDis not compared for them. - Custom hooks
private void writeObject(ObjectOutputStream)andprivate void readObject(ObjectInputStream)allow custom handling;Externalizablerequires a public no-argument constructor. - Never deserialize untrusted data; use
ObjectInputFilterif you must.
Worked example: round trip with transient and static
Statement. Given the class below, what does the program print?
class Book implements Serializable {
String title;
transient int copies = 5;
static String shelf = "A";
Book(String t, int c) { title = t; copies = c; }
}
// main
Book b = new Book("Java 21", 12);
Book.shelf = "B";
// serialize b to bytes, then in the same JVM:
Book.shelf = "C";
Book r = deserialize(bytes);
System.out.println(r.title + " " + r.copies + " " + Book.shelf);
Step 1. title is a normal instance field: it is written and restored as Java 21.
Step 2. copies is transient, so it is not written. On reading, the constructor is not called and the declaration initializer = 5 does not run, so the field gets the default 0.
Step 3. shelf is static, so it is not in the stream. It keeps the current JVM value, C.
Solution. The output is Java 21 0 C.
Why the other choices are wrong. Java 21 12 C forgets transient. Java 21 5 C assumes the initializer runs, which only happens when a constructor is called. Java 21 0 B assumes static state is saved at serialization time.
Second worked example: path arithmetic
Statement. What do these lines print?
Path base = Path.of("/shop/reports");
Path file = Path.of("/shop/archive/../reports/2026/q3.csv");
System.out.println(base.relativize(file));
System.out.println(base.relativize(file.normalize()));
System.out.println(file.normalize().getNameCount());
Step 1. Without normalization, relativize compares names textually. base has names shop, reports; file has shop, archive, .., reports, 2026, q3.csv. They share only shop, so the result climbs one level from reports (..) and descends the remaining names of file: ../archive/../reports/2026/q3.csv.
Step 2. After normalize, file becomes /shop/reports/2026/q3.csv, and the relative path is 2026/q3.csv.
Step 3. The normalized path has four names: shop, reports, 2026, q3.csv.
Solution. The three lines print ../archive/../reports/2026/q3.csv, 2026/q3.csv and 4.
Why other answers are wrong. Assuming relativize normalizes automatically gives 2026/q3.csv for the first line, which is wrong because Path operations are syntactic. Counting the root as a name would give 5.
Common pitfalls and exam tips
- Exam tip: distinguish syntactic
Pathmethods (no I/O, no exceptions about existence) fromFilesmethods (touch the disk and throwIOExceptionsubclasses). Files.lines,list,walkandfindreturn streams that hold open handles: use try-with-resources.resolvewith an absolute argument returns the argument.relativizeneeds two paths of the same kind.Files.copythrowsFileAlreadyExistsExceptionunlessREPLACE_EXISTING;Files.deletethrowsNoSuchFileException,deleteIfExistsdoes not.- Copying a directory copies no content; deleting a non-empty directory fails with
DirectoryNotEmptyException. FileWritertruncates unless the append flag istrue.- Forgetting
close()orflush()on a buffered writer loses data. transientandstaticfields are not serialized; constructors of serializable classes are skipped.- A serializable class with a non-serializable field type fails at run time, not at compile time.
System.console()can benull.
Mini case study: archiving old invoices
Scenario. The bookshop keeps invoices as .txt files under /shop/invoices/YYYY/MM/. Once a month, invoices older than the current year must be moved to /shop/archive/YYYY/ keeping the sub-folder layout, and a manifest file listing the moved files and their sizes must be written. Invoice objects are also cached on disk as .ser files.
Questions. (1) Which API walks the tree? (2) How do you compute each target path? (3) What happens on a second run, and how do you make the move safe? (4) What is wrong with caching a Customer object that holds a Connection field?
Solution.
- Use
Files.walk(Path.of("/shop/invoices"))inside try-with-resources, filtered withFiles::isRegularFileand the extension, because the stream must be closed. - For each source,
Path rel = root.relativize(source), thenPath target = archiveRoot.resolve(rel). Create parents withFiles.createDirectories(target.getParent())before the move. - A plain
Files.movefails withFileAlreadyExistsExceptionif the target is already there. UseStandardCopyOption.REPLACE_EXISTINGif overwriting is acceptable, or skip the file. Because a moved file no longer exists in the source tree, a second run finds nothing new. The manifest can be written withFiles.write(manifest, lines), usingAPPENDwhen several runs add to the same file. - A
Connectionis notSerializable, so writing the object throwsNotSerializableException. Mark the fieldtransientand reopen the connection lazily after reading, or better, do not store infrastructure objects in cached state.
Key terms
- Byte stream / character stream: classes that read or write raw bytes / text characters.
- Decorator (wrapper): a stream that adds buffering or conversion around another stream.
- Path: an immutable, file-system-aware representation of a location, which may not exist.
- Files: the utility class with static methods that perform file operations.
- Normalize: remove redundant
.and..elements lexically. - Serialization: converting an object graph to bytes with
ObjectOutputStream. - transient: modifier that excludes an instance field from default serialization.
- serialVersionUID: the version identifier checked when deserializing.
Summary checklist
- I can choose between byte and character streams and wrap them with buffers.
- I know
read()returns -1 at the end andreadLine()returnsnull. - I can read and write files with
Filesand know the default open options. - I can predict results of
resolve,relativize,normalize,getNameCount,subpath. - I know the exceptions of
copy,move,deleteandcreateFile. - I know
list,walkandfindreturn streams that must be closed. - I can predict what is restored after deserialization (
transient,static, constructors). - I know the role of
serialVersionUIDandNotSerializableException.
Official sources
- Oracle exam page 1Z0-830 (objectives): https://education.oracle.com/java-se-21-developer-professional/pexam_1Z0-830 (opens in a new tab)
java.nio.filepackage: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/package-summary.html (opens in a new tab)FilesAPI: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/Files.html (opens in a new tab)PathAPI: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/Path.html (opens in a new tab)java.iopackage: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/package-summary.html (opens in a new tab)- Java Object Serialization Specification: https://docs.oracle.com/en/java/javase/21/docs/specs/serialization/index.html (opens in a new tab)
- JEP 400, UTF-8 by default: https://openjdk.org/jeps/400 (opens in a new tab)