Java I/O and NIO.2 practice questions

From OCP Java SE 21 (1Z0-830) · 16 questions on this topic

Java I/O and NIO.2 practice questions from OCP Java SE 21 (1Z0-830). This pack has 16 questions tagged Java I/O and NIO.2, drawn from its timed mock exams. 8 of them are worked through in full below — the question, every option, why each is right or wrong, and the explanation.

Worked examples for Java I/O and NIO.2

  1. Question 1

    The class below round-trips an object through `ObjectOutputStream` and `ObjectInputStream`, using a `ByteArrayOutputStream` as the in-memory transport. What is printed to standard output? ```java import java.io.*; public class Main implements Serializable { private static final long serialVersionUID = 1L; private String name; private transient int score; public Main(String name, int score) { this.name = name; this.score = score; } public static void main(String[] args) throws Exception { Main obj = new Main("Alice", 95); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(obj); oos.close(); ObjectInputStream ois = new ObjectInputStream( new ByteArrayInputStream(baos.toByteArray())); Main restored = (Main) ois.readObject(); ois.close(); System.out.println(restored.name + " " + restored.score); } } ```

    1. A. Alice 95

      Assumes `transient` is decorative and that `ObjectOutputStream` serializes every field regardless. In reality `transient` is an explicit instruction to the serialization mechanism to skip the field entirely; no bytes for `score` are ever written to the stream.

    2. B. null 95

      Reverses which field carries the `transient` modifier. It is `score` that is declared `transient` — not `name`. `name` is serialized and restored faithfully as `"Alice"`; `score` is the one that resets to its primitive default.

    3. C. Alice 0Correct answer

      `name` is a normal instance field, so `ObjectOutputStream` writes it and `ObjectInputStream` restores it as `"Alice"`. `score` is declared `transient`, so it is excluded from the stream; when the object is reconstructed `score` is never assigned by the stream and holds the JVM default for `int`, which is `0` (JLS §4.12.5). The result is `Alice 0`.

    4. D. Throws `NotSerializableException` at runtime

      `NotSerializableException` is thrown by `ObjectOutputStream` only when an object's class does not implement `java.io.Serializable`. `Main` explicitly declares `implements Serializable`, satisfying the contract, so the serialization round-trip completes without error.

    Explanation

    The Java serialization mechanism, governed by `ObjectOutputStream` and `ObjectInputStream`, writes and reads the non-static, non-transient fields of a `Serializable` class. A field marked `transient` is silently skipped during the write phase; no bytes for it appear in the stream. When `ObjectInputStream` reconstructs the object it allocates a new instance without calling the constructor and then populates only the fields present in the stream, leaving every `transient` field at its JVM-assigned default — `0` for `int`, `null` for reference types, `false` for `boolean` (JLS §4.12.5). The `serialVersionUID` field is a version tag used by the stream protocol to detect class-layout mismatches; it does not influence what is written or the default values restored.

  2. Question 2

    A four-character String is encoded to bytes as UTF-8, then those bytes are pushed back through a Reader that has been told they are ISO-8859-1. The program prints the character count of the original String, the byte count, the character count after decoding, and the code point of the last decoded character. What is printed? ```java import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.StandardCharsets; public class Main { public static void main(String[] args) throws IOException { String text = "caf" + (char) 0xE9; byte[] data = text.getBytes(StandardCharsets.UTF_8); StringBuilder sb = new StringBuilder(); try (Reader r = new InputStreamReader(new ByteArrayInputStream(data), StandardCharsets.ISO_8859_1)) { int c; while ((c = r.read()) != -1) { sb.append((char) c); } } System.out.println(text.length() + " " + data.length + " " + sb.length() + " " + (int) sb.charAt(sb.length() - 1)); } } ```

    1. A. 4 4 4 233

      Assumes e occupies one byte in UTF-8 and the round-trip is lossless. That holds only if the encoding were ISO-8859-1 on both sides; UTF-8 encodes U+00E9 as two bytes, so the byte and char counts are 5, not 4.

    2. B. 4 5 5 233

      Gets the counts right but assumes the mis-decoded text still ends in e-acute. It ends with the character from the last byte 0xA9 = U+00A9 = 169; the 0xC3 lead byte became a separate character earlier in the sequence.

    3. C. 4 5 5 169Correct answer

      The 4-char String encodes to 5 UTF-8 bytes (the accented char is two); decoding all 5 as single-byte ISO-8859-1 yields 5 characters, the last from byte 0xA9 = U+00A9 = 169, the copyright sign.

    4. D. 4 5 4 233

      Assumes an InputStreamReader detects the real encoding or repairs the mismatch. The charset handed to the Reader is taken as fact with no sniffing, so all 5 bytes decode to 5 characters.

    Explanation

    Trace: the String is `caf` plus U+00E9 (é) — four chars, so `text.length()` is `4`. UTF-8 is variable width: `c`, `a`, `f` are one byte each, but U+00E9 needs two (0xC3 0xA9), so `data.length` is `5`. ISO-8859-1 is a single-byte charset that maps every byte value to the code point of the same number, so decoding those five bytes as ISO-8859-1 yields five characters, never four — `sb.length()` is `5`. The last of them comes from the trailing byte 0xA9, which decodes to U+00A9, decimal `169` (the © sign). Output: `4 5 5 169`. Why the others are wrong: `4 4 4 233` assumes é occupies one byte in UTF-8 and that the round-trip is lossless. It would be right if the encoding were ISO-8859-1 on both sides; UTF-8 only agrees with ISO-8859-1 for the ASCII range. `4 5 4 233` assumes an InputStreamReader detects the real encoding of the bytes (or that Java repairs the mismatch). It does not — the charset you hand the Reader is taken as fact, and there is no sniffing or BOM inspection here. `4 5 5 233` gets the counts right but assumes the mis-decoded text still ends in é. It ends with the character decoded from the LAST byte, 0xA9 = 169; the 0xC3 lead byte became a separate character (Ã) earlier in the sequence. Exam tip: a charset mismatch does not throw — it silently produces the wrong characters, and with a single-byte charset like ISO-8859-1 no byte is ever invalid, so nothing can even detect the problem. Count bytes on the OutputStream/InputStream side and characters on the Reader/Writer side; the two are only equal for pure ASCII. The reverse trap: decoding those same bytes as US-ASCII does not throw either — CharsetDecoder's default action REPLACE turns each unmappable byte into U+FFFD.

  3. Question 3

    What does the following program print? ```java import java.nio.file.Path; public class Main { public static void main(String[] args) { Path p = Path.of("a/b/../../c"); System.out.println(p.normalize()); } } ```

    1. A. a/b/../../c

      This is the string form of the original path, as if normalize() were a no-op. Path.normalize() always eliminates . and .. elements syntactically; it never leaves redundant components in place.

    2. B. a/c

      This results from applying only one of the two .. components — cancelling b but leaving a unchanged. Each .. independently cancels its immediately preceding name element: the first removes b and the second removes a, so both are eliminated before c is reached.

    3. C. a/b/c

      This results from treating .. as literal characters to erase rather than as a directory-traversal instruction. Simply removing the characters ../.. from the path string yields a/b/c, but the correct interpretation is that each .. ascends one directory level and cancels the preceding name element.

    4. D. cCorrect answer

      normalize() processes name elements with a stack: a is pushed, b is pushed, the first .. pops b, the second .. pops a, and c is pushed, leaving c as the sole element. Because the input is relative and the result is a single component, the output contains no path separator and is identical on all platforms.

    Explanation

    Path.normalize() is a purely syntactic operation that requires no filesystem access and works on the in-memory path elements alone. It uses a stack: each name element is pushed, a . is discarded, and each .. pops the most recently pushed element (or is retained if the stack is empty and the path is relative). Processing a/b/../../c left to right pushes a and b, then two consecutive .. operations clear the stack, and c is finally pushed, yielding a path whose sole element is c. The output is the same on all platforms because the result is a single component with no separator.

  4. Question 4

    What does the following program print? ```java import java.nio.file.Path; public class Main { public static void main(String[] args) { Path base = Path.of("/home/user"); Path result = base.resolve("/etc/config"); System.out.println(result.startsWith("/home")); } } ```

    1. A. true

      This assumes resolve() always appends its argument to the receiver, making the result /home/user/etc/config, which would start with /home. When the argument has a root component, the Javadoc specifies it is returned directly and the receiver is discarded, so the result is /etc/config, which does not start with /home.

    2. B. falseCorrect answer

      When the argument to resolve() has a root component, the Javadoc specifies it is returned directly and the receiver is discarded. The result is therefore /etc/config, which does not start with /home, so startsWith returns false.

    3. C. Compilation fails

      Both resolve(String) and startsWith(String) are valid Path methods, and System.out.println(boolean) is valid. The code compiles and runs without error.

    4. D. The program throws IllegalArgumentException

      Path.resolve(String) does not throw when given a path with a root component; handling such an argument by returning it directly is documented behaviour, not an error condition.

    Explanation

    Path.resolve(other) is designed to anchor a relative path against a base. When the argument has a root component it is self-contained and needs no base, so the Javadoc specifies it is returned directly and the receiver is discarded. The result path therefore originates from the resolved argument rather than from the receiver, which the startsWith check confirms by returning false.

  5. Question 5

    A PrintWriter wraps an in-memory ByteArrayOutputStream. The size of the byte sink is sampled at three points, and one print() call happens after the writer has been closed. What is printed? ```java import java.io.ByteArrayOutputStream; import java.io.PrintWriter; public class Main { public static void main(String[] args) { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); PrintWriter writer = new PrintWriter(bytes); writer.print("abc"); System.out.print(bytes.size() + " "); writer.flush(); System.out.print(bytes.size() + " "); writer.print("de"); writer.close(); writer.print("fgh"); System.out.println(bytes.size() + " " + writer.checkError()); } } ```

    1. A. An unhandled IOException ("Stream closed") is thrown by the print call that follows close()

      Assumes PrintWriter throws on I/O failure. PrintWriter is contractually silent — every method suppresses IOException and raises an internal error flag instead, so no exception escapes and checkError() is how you learn a write failed.

    2. B. 0 3 3 true

      Assumes close() discards whatever is still buffered. close() flushes first, then closes, so the buffered "de" reaches the array, taking it to 5 bytes.

    3. C. 3 3 5 true

      Assumes a PrintWriter writes straight through to its sink. A PrintWriter built on an OutputStream buffers, so the first sample is 0 (nothing flushed yet), not 3.

    4. D. 0 3 5 trueCorrect answer

      The PrintWriter buffers, so print("abc") leaves the array at 0; flush() pushes 3 bytes; print("de") plus close() flushes to 5; the final print on the closed writer fails silently, leaving the array at 5 and setting checkError() to true.

    Explanation

    Trace: `new PrintWriter(OutputStream)` interposes a BufferedWriter, so `print("abc")` only fills the character buffer — nothing has reached the byte array yet, and the first sample is `0`. `flush()` pushes those three characters down, so the second sample is `3`. Then `print("de")` buffers again and `close()` flushes before closing, taking the array to 5 bytes. The final `print("fgh")` is made on a closed writer: the underlying write fails, but PrintWriter never propagates IOException — it swallows it and raises its internal error flag, so the array stays at 5 and `checkError()` reports `true`. Output: `0 3 5 true`. Why the others are wrong: `3 3 5 true` assumes a PrintWriter writes straight through to its sink; it does not — a PrintWriter built on an OutputStream buffers, and only flush(), close(), or a full buffer moves bytes. `0 3 3 true` assumes close() discards whatever is still buffered. close() flushes first, then closes, so the buffered `de` is never lost. `An unhandled IOException ("Stream closed")...` encodes the belief that PrintWriter throws on I/O failure. It is the one writer in java.io that is contractually silent: every PrintWriter method suppresses IOException, and checkError() is the only way to learn that a write failed. Exam tip: PrintWriter (and PrintStream) trade exceptions for a sticky error flag — quiet failure is the whole point of the class, which is why System.out never forces you into a try/catch. The reverse trap is assuming the flag can be cleared or that checkError() is cheap: it flushes the stream as a side effect, and once set the flag stays set.

  6. Question 6

    What does the following program print? ```java import java.nio.file.Path; public class Main { public static void main(String[] args) { Path base = Path.of("/home/user/documents"); Path target = Path.of("/home/user/documents/report.txt"); System.out.println(base.relativize(target)); } } ```

    1. A. report.txtCorrect answer

      Path.relativize(target) constructs the relative path to navigate from the receiver to the argument. Both paths share the common prefix /home/user/documents; the only remaining element of target beyond that prefix is report.txt. Because the result is a single name element it contains no platform-dependent separator, so the output is the same on every operating system.

    2. B. /home/user/documents/report.txt

      Path.relativize() always returns a relative path, never an absolute one. Returning the argument unchanged would defeat the purpose of the method; the Javadoc specifies the result has no root component.

    3. C. ..

      This is the result of the reverse call — target.relativize(base). To navigate from /home/user/documents/report.txt back up to its parent /home/user/documents requires ascending one level. Swapping receiver and argument reverses the direction of travel.

    4. D. documents/report.txt

      This would be the result if the receiver were /home/user instead of /home/user/documents. With the correct receiver, the documents segment is already part of the shared prefix, so only report.txt lies beyond it.

    Explanation

    Path.relativize(target) answers the question 'starting from this path, what sequence of steps reaches target?' The receiver is the origin and the argument is the destination. Both paths share /home/user/documents as their full common prefix; the only remaining element of target beyond that prefix is report.txt. Because the result is a single name element it carries no path-separator character, making the printed output identical on every platform.

  7. Question 7

    What does this print? ```java import java.nio.file.*; public class Main { public static void main(String[] args) { Path p = Path.of("users", "docs", "notes.txt"); System.out.println(p.getNameCount() + " " + p.getFileName()); } } ```

    1. A. 2 notes.txt

      Every name element counts, including the file name itself, so the count is 3, not 2.

    2. B. 3 users

      users is getName(0), the element closest to the root, not what getFileName() returns.

    3. C. Compilation fails: Path.of does not accept multiple strings

      Path.of(String first, String... more) is the standard varargs factory, so the call compiles fine.

    4. D. 3 notes.txtCorrect answer

      The varargs join into the relative path users/docs/notes.txt with three name elements (no root), so getNameCount() is 3 and getFileName() returns the farthest element, notes.txt.

    Explanation

    Path.of joins its varargs into one relative path whose name elements are the supplied segments. Every segment counts toward the name count, including the file name, and no root is present here, so the root is never a name element. getFileName returns the element farthest from the root, which is the trailing segment.

  8. Question 8

    What does this print? ```java import java.nio.file.*; public class Main { public static void main(String[] args) { System.out.println(Path.of("a/b").resolve("../c").normalize().toString().replace('\\', '/')); } } ```

    1. A. a/b/../c

      This is the intermediate result before normalize runs; the code calls both resolve and normalize.

    2. B. c

      Producing c alone would require two .. elements; a single .. removes exactly one preceding name.

    3. C. a/cCorrect answer

      The relative argument ../c is appended to a/b giving a/b/../c, then normalize lets the .. consume the preceding b, leaving a/c.

    4. D. a/b/c

      This would mean the .. was silently ignored instead of cancelling the preceding b.

    Explanation

    Chained Path calls evaluate strictly left to right, so write down the intermediate path after each step. A relative argument is first appended to the receiver, producing a path that still contains the .. element; normalize then lets that .. cancel the single name before it. Applying the two operations in the opposite order would give a different result.

Practise all 16 Java I/O and NIO.2 questions

OCP Java SE 21 has the full set, inside timed mock exams that mirror real exam conditions — every question with a worked explanation.

Open OCP Java SE 21

Other topics in this pack