1Z0-830 I/O and NIO.2 - Java SE 21 Certification Prep
java.io package provides byte streams, character streams, and serialization. The newer java.nio.file package (NIO.2, introduced in Java 7) provides the Path and Files APIs, which are more powerful, more consistent, and the preferred approach for new code. The 1Z0-830 exam tests both generations, so you need to know when to use each and how they differ.
I/O Overview
Java I/O is built around a layered stream model. Low-level streams read or write raw bytes or characters. Wrapper streams add buffering, formatting, or data-type awareness on top of them. Choosing the right layer for the job is a common exam theme.
Stream Type Summary:
- Byte Streams - InputStream / OutputStream - for binary data such as images, audio, or serialized objects
- Character Streams - Reader / Writer - for text data; handle character encoding automatically
- Buffered Streams - BufferedInputStream, BufferedReader, etc. - reduce system calls by batching reads and writes
- Data Streams - DataInputStream / DataOutputStream - read and write Java primitives (int, double, boolean, etc.) in a portable binary format
- Object Streams - ObjectInputStream / ObjectOutputStream - serialize and deserialize full Java objects
// Classic java.io stream hierarchy // Byte streams - work with raw bytes (8-bit) InputStream / OutputStream FileInputStream / FileOutputStream // File access BufferedInputStream / BufferedOutputStream // Buffered for efficiency DataInputStream / DataOutputStream // Primitives in binary form ObjectInputStream / ObjectOutputStream // Object serialization // Character streams - work with characters (16-bit Unicode) Reader / Writer FileReader / FileWriter // File access using default charset BufferedReader / BufferedWriter // Buffered for efficiency InputStreamReader / OutputStreamWriter // Bridge: bytes to characters with charset PrintWriter // Formatted text output
Exam Tip:
InputStreamReader and OutputStreamWriter are bridge classes. They wrap a byte stream and convert it to a character stream using a specified (or default) charset. Use them when you need to control the character encoding explicitly, for example when reading UTF-8 files on a system whose default charset is something else.
Path and Files (NIO.2)
NIO.2 represents file system locations using thePath interface rather than the older File class. A Path is just a reference to a location - it does not require the file to actually exist. Path objects are immutable; all operations return a new Path rather than modifying the original.
// Creating Path objects
Path path1 = Path.of("data", "file.txt"); // Relative path (Java 11+)
Path path2 = Path.of("/users/data/file.txt"); // Absolute path
Path path3 = Paths.get("data", "file.txt"); // Older equivalent (still valid)
Path path4 = Path.of("data/file.txt"); // Single string with separator
// Inspecting path components
Path path = Path.of("/users/john/documents/file.txt");
path.getFileName(); // file.txt (last element only)
path.getParent(); // /users/john/documents
path.getRoot(); // / (null for relative paths)
path.getNameCount(); // 4 (users, john, documents, file.txt)
path.getName(0); // users (index 0 = first element after root)
path.getName(2); // documents
path.subpath(1, 3); // john/documents (end index is exclusive)
path.isAbsolute(); // true
// Combining and transforming paths
Path base = Path.of("/users/john");
Path resolved = base.resolve("file.txt"); // /users/john/file.txt
Path sibling = path.resolveSibling("other.txt"); // /users/john/documents/other.txt
// relativize() computes the path FROM the argument TO this path
Path relative = Path.of("/users/john/documents/file.txt")
.relativize(Path.of("/users/john")); // ../..
// normalize() removes redundant . and .. elements
Path normalized = Path.of("./data/../data/file.txt").normalize(); // data/file.txt
// toRealPath() resolves symlinks and returns the canonical absolute path
// (file must exist - throws IOException if not)
Path real = path.toRealPath();
// toAbsolutePath() prepends the working directory - does not resolve symlinks
Path absolute = path.toAbsolutePath();
// Comparing paths
path1.equals(path2); // Compares path strings, not file system entries
path1.startsWith("/users"); // true if this path begins with the given path
path1.endsWith("file.txt"); // true if this path ends with the given path
Exam Tip:
Path.equals() compares the path string, not the underlying file. Two paths pointing to the same file via different strings (for example, one absolute and one relative) will NOT be equal according to equals(). Use Files.isSameFile() to test whether two paths refer to the same physical file.
Files Class
TheFiles utility class is the primary way to interact with the file system in NIO.2. It provides static methods for creating, copying, moving, deleting, reading, and writing files, as well as for querying file attributes. Most of its methods throw IOException, so they must be called inside a try-catch or declared in a throws clause.
// Checking existence and type
Path path = Path.of("data/file.txt");
Files.exists(path); // true if file or directory exists
Files.notExists(path); // true if definitely does not exist (not just inaccessible)
Files.isDirectory(path);
Files.isRegularFile(path);
Files.isReadable(path);
Files.isWritable(path);
Files.isExecutable(path);
Files.isSymbolicLink(path);
Files.isHidden(path);
// File metadata
Files.size(path); // Size in bytes
Files.getLastModifiedTime(path);
Files.setLastModifiedTime(path, FileTime.fromMillis(System.currentTimeMillis()));
Files.getOwner(path);
// Creating files and directories
Files.createFile(path); // Throws if file already exists
Files.createDirectory(Path.of("newdir")); // Throws if parent does not exist
Files.createDirectories(Path.of("a/b/c")); // Creates all missing parent directories
// Copying files
Files.copy(source, target); // Throws if target exists
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); // Overwrites target
Files.copy(inputStream, path); // Stream to file
Files.copy(path, outputStream); // File to stream
// Moving and renaming
Files.move(source, target);
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); // OS-level atomic rename
// Deleting
Files.delete(path); // Throws NoSuchFileException if path does not exist
Files.deleteIfExists(path); // Returns true if deleted, false if not found - never throws
// Convenience read/write methods for small files
String content = Files.readString(path); // Entire file as String (Java 11+)
Files.writeString(path, "content"); // Write String to file
Files.writeString(path, "append", StandardOpenOption.APPEND); // Append String
List<String> lines = Files.readAllLines(path); // All lines into a List
Files.write(path, lines); // Write List of lines
byte[] bytes = Files.readAllBytes(path); // Entire file as byte array
Files.write(path, bytes); // Write byte array
Exam Tip:
Files.copy() copies a single file only. It does NOT recursively copy directory contents - calling it on a directory creates an empty directory at the target. To copy a full directory tree, use Files.walk() combined with individual Files.copy() calls. Also, Files.delete() on a non-empty directory throws DirectoryNotEmptyException - you must delete the contents first.
Reading Files
NIO.2 provides several ways to read file contents, ranging from low-level byte streams to high-level convenience methods. Choose based on file size and whether you need streaming or in-memory access. Always close streams using try-with-resources.
// Traditional BufferedReader wrapping FileReader
// FileReader uses the platform default charset
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line;
while ((line = reader.readLine()) != null) { // readLine() returns null at EOF
System.out.println(line);
}
}
// Preferred NIO.2 approach - Files.newBufferedReader() uses UTF-8 by default
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
// Files.lines() - lazy streaming; reads one line at a time
// The stream MUST be closed - use try-with-resources
try (Stream<String> lines = Files.lines(path)) {
lines.filter(line -> !line.isBlank())
.map(String::trim)
.forEach(System.out::println);
}
// Files.readAllLines() - loads the entire file into a List in memory
// Only use for small files; throws OutOfMemoryError for very large files
List<String> allLines = Files.readAllLines(path);
// Files.readString() - entire file contents as a single String (Java 11+)
// Also only suitable for small files
String content = Files.readString(path);
// Low-level byte stream - reads one byte at a time (inefficient without buffering)
try (InputStream is = new FileInputStream("file.bin")) {
int byteRead;
while ((byteRead = is.read()) != -1) { // read() returns -1 at EOF
// Process single byte
}
}
// Buffered byte reading - reads a chunk at a time for better performance
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("file.bin"))) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
// Valid data is in buffer[0] through buffer[bytesRead - 1]
// Do not assume the buffer is always full
}
}
Exam Tip:
Files.lines() and Files.list() both return a Stream that holds an open file handle. Failing to close the stream leaks a file descriptor. Always wrap them in try-with-resources. Files.readAllLines() closes the file automatically because it reads everything before returning.
Writing Files
Writing works symmetrically to reading. The NIO.2 methods default to UTF-8 and are generally preferred over the classicFileWriter-based approach, which uses the platform default charset and can produce inconsistent results across environments. Use StandardOpenOption flags to control whether to append, truncate, or create files.
// Traditional BufferedWriter - uses platform default charset
try (BufferedWriter writer = new BufferedWriter(new FileWriter("file.txt"))) {
writer.write("Hello");
writer.newLine(); // Writes OS-appropriate line separator (\n or \r\n)
writer.write("World");
}
// Preferred NIO.2 approach - Files.newBufferedWriter() uses UTF-8 by default
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
writer.write("Hello");
writer.newLine();
}
// Appending to an existing file
// CREATE creates the file if it does not exist; APPEND positions at end of file
try (BufferedWriter writer = Files.newBufferedWriter(path,
StandardOpenOption.APPEND, StandardOpenOption.CREATE)) {
writer.write("Appended line");
}
// Files.writeString() convenience method - no stream to close
Files.writeString(path, "Content"); // Overwrites by default
Files.writeString(path, "More content", StandardOpenOption.APPEND);
// PrintWriter - useful for formatted output
try (PrintWriter pw = new PrintWriter(new FileWriter("file.txt"))) {
pw.println("Line 1"); // Adds newline automatically
pw.printf("Number: %d%n", 42); // printf-style formatting
}
// Low-level byte output stream
try (OutputStream os = new FileOutputStream("file.bin")) {
os.write(65); // Write single byte (A in ASCII)
os.write(new byte[]{1, 2, 3}); // Write byte array in one call
}
// Buffered byte output - batches writes to reduce system calls
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("file.bin"))) {
bos.write(data);
// Data is flushed when the buffer fills or the stream is closed
}
Exam Tip:
FileWriter and FileReader use the platform default charset, which varies by operating system. Files.newBufferedWriter() and Files.newBufferedReader() use UTF-8 by default, making them safer for cross-platform code. On the exam, if a question specifies encoding behavior, look at which API is being used.
Directory Operations
NIO.2 provides stream-based methods for listing and traversing directory trees. The key distinction to remember is thatFiles.list() is not recursive while Files.walk() is. Both return a Stream<Path> that must be closed.
// Files.list() - lists immediate children only (not recursive)
try (Stream<Path> stream = Files.list(Path.of("."))) {
stream.forEach(System.out::println);
}
// Files.walk() - recursively visits all descendants
// Includes the starting directory itself as the first element
try (Stream<Path> stream = Files.walk(Path.of("."))) {
stream.filter(Files::isRegularFile)
.forEach(System.out::println);
}
// Files.walk() with a maximum depth limit
// Depth 0 returns only the starting path itself
// Depth 1 is equivalent to Files.list()
try (Stream<Path> stream = Files.walk(Path.of("."), 2)) {
stream.forEach(System.out::println);
}
// Files.find() - walk with a built-in BiPredicate filter
// Second argument is max depth, third is the filter (Path + BasicFileAttributes)
try (Stream<Path> stream = Files.find(Path.of("."), 10,
(p, attrs) -> attrs.isRegularFile() &&
p.toString().endsWith(".java"))) {
stream.forEach(System.out::println);
}
// DirectoryStream with a glob pattern - classic NIO.2 approach
// Glob patterns: * matches any sequence within a name component
// ** matches across directory boundaries
try (DirectoryStream<Path> stream =
Files.newDirectoryStream(Path.of("."), "*.txt")) {
for (Path p : stream) {
System.out.println(p);
}
}
// Deleting a directory tree (must delete contents before the directory itself)
try (Stream<Path> stream = Files.walk(Path.of("targetDir"))) {
stream.sorted(Comparator.reverseOrder()) // Files before their parent directories
.forEach(p -> {
try { Files.delete(p); }
catch (IOException e) { throw new UncheckedIOException(e); }
});
}
Exam Tip:
Files.list() is NOT recursive - it only lists immediate children of the directory. Files.walk() is recursive and includes the starting directory itself. Both return a Stream<Path> backed by an open directory handle, so they must be closed with try-with-resources. Files.find() is like Files.walk() but with an integrated filter predicate that also gives you access to file attributes without a separate stat call.
Serialization
Java serialization converts an object graph into a byte stream so it can be stored or transmitted, and later reconstructed. A class must implementSerializable to participate. The exam focuses on the rules around which fields are serialized, how transient and static affect this, and how serialVersionUID prevents version conflicts.
// A serializable class
public class Person implements Serializable {
// serialVersionUID is used to verify that the sender and receiver of a
// serialized object have compatible class definitions.
// If you change the class and the UID does not match, deserialization
// throws InvalidClassException.
private static final long serialVersionUID = 1L;
private String name; // Serialized normally
private int age; // Serialized normally
private transient String password; // NOT serialized - gets null on deserialization
private static int count; // NOT serialized - static fields belong to the class
}
// Writing (serializing) an object to a file
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("person.ser"))) {
Person person = new Person("Alice", 30);
oos.writeObject(person);
}
// Reading (deserializing) an object from a file
// readObject() returns Object - must cast to the target type
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("person.ser"))) {
Person person = (Person) ois.readObject(); // Throws ClassNotFoundException
}
// Serialization rules to memorize for the exam:
// 1. The class must implement Serializable (marker interface - no methods)
// 2. All non-transient, non-static instance fields must also be serializable
// 3. static fields are NEVER serialized (they belong to the class, not the object)
// 4. transient fields are skipped during serialization
// 5. On deserialization, transient fields receive their default values
// (null for objects, 0 for numbers, false for booleans)
// 6. The no-arg constructor is NOT called during deserialization for Serializable classes
// 7. If a superclass does not implement Serializable, its no-arg constructor IS
// called during deserialization to initialize its fields
// Custom serialization - override these private methods to control the process
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // Serialize all non-transient fields normally
oos.writeObject(encrypt(password)); // Then write the encrypted password manually
}
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // Deserialize normal fields first
password = decrypt((String) ois.readObject()); // Then read and decrypt the password
}
Exam Tip: The constructor is NOT called when deserializing a
Serializable object. The JVM reconstructs the object by reading the byte stream directly. However, if a superclass in the hierarchy does NOT implement Serializable, that superclass's no-arg constructor IS called to initialize the inherited fields. This is a commonly tested edge case.
Console I/O
TheConsole class provides a way to read from and write to the terminal interactively. Its most important feature is readPassword(), which disables terminal echo so typed characters are not displayed. Note that System.console() returns null when the program is not connected to a real terminal, such as when running inside most IDEs or when piping input from a file.
// Console - may be null when not connected to a terminal (e.g., inside an IDE)
Console console = System.console();
if (console != null) {
String name = console.readLine("Enter name: "); // Prompt + read a line
char[] password = console.readPassword("Password: "); // No echo - returns char[]
// Use char[] for passwords and clear it after use:
// Arrays.fill(password, '\0');
console.printf("Hello, %s%n", name);
}
// Scanner - works in all environments including IDEs
Scanner scanner = new Scanner(System.in);
String line = scanner.nextLine(); // Reads an entire line including spaces
int number = scanner.nextInt(); // Reads next token as int (leaves newline in buffer)
double d = scanner.nextDouble(); // Reads next token as double
boolean hasMore = scanner.hasNextLine(); // Checks without consuming
// Standard streams
System.out.println("Output"); // PrintStream connected to stdout
System.err.println("Error"); // PrintStream connected to stderr (unbuffered)
int input = System.in.read(); // Reads a single byte from stdin
// PrintStream formatting methods (same on System.out and on any PrintStream/PrintWriter)
System.out.print("No newline");
System.out.println("Appends newline");
System.out.printf("Formatted: %s %d%n", "text", 42); // %n is platform-appropriate newline
System.out.format("Same as printf: %s%n", "text"); // format() is an alias for printf()
Exam Tip: After calling
scanner.nextInt() or scanner.nextDouble(), the newline character that the user typed is left in the input buffer. If you then call scanner.nextLine(), it will immediately return an empty string because it reads up to that leftover newline. The fix is to call scanner.nextLine() once to consume the leftover newline before reading the next full line of input.
1Z0-830 Java SE 21 Certification - Table of Contents
Master all exam topics with comprehensive study guides and practice examples.
Popular Posts
1Z0-830 Java SE 21 Developer Certification
Azure AI Foundry Hello World
Azure AI Agent Hello World
Foundry vs Hub Projects
Build Agents with SDK
Bing Web Search Agent
Function Calling Agent
Spring Boot + Azure Key Vault Hello World Example
Spring Boot + Elasticsearch + Azure Key Vault Example
Spring Boot Azure AD (Entra ID) OAuth 2.0 Authentication
Deploy Spring Boot App to Azure App Service
Secure Azure App Service using Azure API Management
Deploy Spring Boot JAR to Azure App Service
Deploy Spring Boot + MySQL to Azure App Service
Spring Boot + Azure Managed Identity Example
Secure Spring Boot Azure Web App with Managed Identity + App Registration
Elasticsearch 8 Security - Integrate Azure AD OIDC