SEN1002 · Week 11 · Files and Streams, Object Serialization

Files, streams and object serialization

File operations with Files and Path, reading with Scanner, writing with PrintWriter and Formatter, try-with-resources, and saving objects with ObjectOutputStream.

filestreamPathPathsFilesScannerPrintWriterFormatterFileWriterBufferedReadertry-with-resourcesAutoCloseabletext filebinary fileserializationSerializableObjectOutputStreamObjectInputStreamtransient

When a program closes, everything in the variables is lost. The way to make data permanent is to write it to a file. This week you work with two kinds of files: text files that a person can read, and binary files that store Java objects as they are. The most common exam topics: compile errors about checked exceptions (FileNotFoundException, IOException), the closing order of try-with-resources, Scanner loops, and transient in serialization.

Golden rules:

  1. File operations throw checked exceptions. Either try/catch or throws IOException.
  2. Close what you open. The safest way is try-with-resources; a writer that is not closed may not write the data to disk.
  3. new FileWriter(file) erases the old content. To append, use new FileWriter(file, true).
  4. The class to be serialized must be Serializable, and so must the objects it holds.
  5. transient and static fields are not written to the file.

The stream concept

In Java, talking to a file happens through a stream: data flows byte by byte or character by character. There are two main families:

Text (character)Binary (byte)
What is stored?Readable charactersRaw bytes, Java objects
ReadingScanner, BufferedReader, Files.readAllLinesObjectInputStream, FileInputStream
WritingPrintWriter, Formatter, FileWriter, Files.writeObjectOutputStream, FileOutputStream
Opens in Notepad?YesMeaningless characters

Path and Files

Path in the java.nio.file package represents the path of a file or folder. The Files class offers static helper methods.

Path scores = Paths.get("scores.txt");               // or Path.of("scores.txt")
System.out.println(Files.exists(scores) + " " + Files.exists(Paths.get("nope.txt")));
System.out.println(Files.isDirectory(dir) + " " + Files.isRegularFile(scores));
List<String> lines = Files.readAllLines(scores);
System.out.println(lines.size() + " " + lines.get(1));
System.out.println(scores.getFileName());
Path p2 = Paths.get("data", "2026", "notes.txt");
System.out.println(p2.getFileName() + " " + p2.getNameCount());
Files.write(Paths.get("w.txt"), List.of("one", "two"));
System.out.println(Files.readAllLines(Paths.get("w.txt")));
true false
true true
3 Mert 70
scores.txt
notes.txt 3
[one, two]

(scores.txt is the three-line file written in the previous example.) If a path is given as relative, it is looked up relative to the folder the program runs in; an absolute path is a full path like C:\....

Writing a text file

PrintWriter

It has the same methods as System.out: print, println, printf. The difference is that the output goes to a file instead of the screen.

try (PrintWriter out = new PrintWriter("scores.txt")) {
    out.println("Ece 85");
    out.println("Mert 70");
    out.printf("%s %d%n", "Can", 92);
}   // automatic close here: the data is written to disk

new PrintWriter("scores.txt") creates the file if it does not exist, and empties it if it does. If it cannot create it, it throws FileNotFoundException (checked).

What happens if you do not close?

PrintWriter leaky = new PrintWriter(new FileWriter("lost.txt"));
leaky.println("hello");
System.out.println(Files.size(Paths.get("lost.txt")));    // still in the buffer
leaky.close();
System.out.println(Files.size(Paths.get("lost.txt")) > 0);
0
true

Overwriting and appending

try (PrintWriter w = new PrintWriter(new FileWriter("log.txt"))) { w.println("first"); }
try (PrintWriter w = new PrintWriter(new FileWriter("log.txt"))) { w.println("second"); }
System.out.println(Files.readAllLines(Paths.get("log.txt")));
try (PrintWriter w = new PrintWriter(new FileWriter("log.txt", true))) { w.println("third"); }
System.out.println(Files.readAllLines(Paths.get("log.txt")));
[second]
[second, third]

Formatter

For formatted output, the format method uses the same format codes as printf. It is ideal for writing an aligned report like a table.

try (Formatter f = new Formatter("report.txt")) {
    f.format("%-6s|%5.1f|%03d%n", "Ada", 3.456, 7);
    f.format("%-6s|%5.1f|%03d%n", "Bora", 12.0, 42);
}

The lines of the file (the square brackets were added to make the spaces visible):

[Ada   |  3.5|007]
[Bora  | 12.0|042]
FormatMeaning
%s, %d, %fString, integer, decimal
%.2f2 decimal places (rounds)
%8s / %-8sfield of 8 characters, right / left aligned
%05d5 digits, padded with zeros
%nLine end

Reading a text file

With Scanner

You already know how to read from the keyboard; to read from a file, you give Scanner the file instead of System.in.

int total = 0, n = 0;
try (Scanner in = new Scanner(new File("scores.txt"))) {
    while (in.hasNext()) {
        String name = in.next();
        int score = in.nextInt();
        total += score; n++;
        System.out.println(name + " -> " + score);
    }
}
System.out.println("avg=" + (double) total / n);
Ece -> 85
Mert -> 70
Can -> 92
avg=82.33333333333333

If the file does not exist: new Scanner(new File(...)) throws FileNotFoundException, while Files.readAllLines throws NoSuchFileException (both are from the IOException family).

PITFALL · Scanner and the Turkish locale. Scanner reads numbers according to the computer’s locale. On a machine with Turkish settings, 5.5 is not counted as a decimal number, 5,5 is. In the test run, the average of the same file came out as 4.6250 in English settings and 4,3333 in Turkish settings. In the exam, assume the default English; in a real program, write in.useLocale(Locale.US).

Line by line with BufferedReader

try (BufferedReader br = Files.newBufferedReader(Paths.get("scores.txt"))) {
    String line;
    int count = 0;
    while ((line = br.readLine()) != null) count += line.length();
    System.out.println(count);
    System.out.println(br.readLine());     // end of file
}
19
null

readLine returns null when the file ends; the loop condition checks for this. (“Ece 85” is 6, “Mert 70” is 7, “Can 92” is 6 characters: 19.)

try-with-resources

The form try (Resource r = ...) { ... } automatically calls the resource’s close() method however the block ends. The resource must implement the AutoCloseable (or Closeable) interface; otherwise try-with-resources not applicable to variable type.

class Res implements AutoCloseable {
    private final String name;
    Res(String name) { this.name = name; System.out.println("open " + name); }
    void use() { System.out.println("use " + name); }
    @Override public void close() { System.out.println("close " + name); }
}

try (Res a = new Res("A"); Res b = new Res("B")) {
    a.use();
    b.use();
    throw new IllegalStateException("boom");
} catch (IllegalStateException e) {
    System.out.println("caught " + e.getMessage());
} finally {
    System.out.println("finally");
}
open A
open B
use A
use B
close B
close A
caught boom
finally

The order rule: opening is in the order written, closing is in reverse order, and closing happens before the catch. If close() also throws an exception, the exception in the body stays as the main exception, and the one from close is added to it as “suppressed”: in the test run e.getSuppressed()[0].getMessage() gave close failed.

Object serialization

Turning an object, together with all its fields, into a byte array and writing it to a file is called serialization; reading it back and rebuilding the object is called deserialization. The class must implement the java.io.Serializable interface. This interface has no methods at all; it only marks “this class can be written” (marker interface).

class Student implements Serializable {
    private static final long serialVersionUID = 1L;   // version number (recommended)
    String name; double gpa;
    transient String password;                         // not written
    static String school = "BAU";                      // not written (belongs to the class)
    Student(String n, double g, String p) { name = n; gpa = g; password = p; }
    public String toString() { return name + " " + gpa + " " + password; }
}

try (ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("students.ser")))) {
    oos.writeObject(new Student("Deniz", 3.4, "secret"));
    oos.writeObject(new Student("Ece", 3.9, "pw"));
    oos.writeInt(2026);
}
Student.school = "changed";
try (ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get("students.ser")))) {
    Student s1 = (Student) ois.readObject();    // returns Object, cast required
    Student s2 = (Student) ois.readObject();
    int year = ois.readInt();
    System.out.println(s1);
    System.out.println(s2);
    System.out.println(year + " " + Student.school);
}
Deniz 3.4 null
Ece 3.9 null
2026 changed

The reading order must be the same as the writing order. readObject can throw two checked exceptions: IOException and ClassNotFoundException. If there is no object left to read in the file, EOFException comes.

Collections such as ArrayList are also Serializable (as long as the elements inside are Serializable too). You can write the whole list with a single writeObject. The list read back is a new object: in the test run back == names came out false and back.equals(names) came out true.

If the class of a field inside is not Serializable, NotSerializableException is thrown at write time; its message is the name of the problem class. This is a run-time error, the compiler does not catch it.

Common mistakes

Wrong ideaThe truth
new Scanner(new File(...)) can be written freelyFileNotFoundException is checked; catch or throws
I wrote to the file, it is fine if I do not closeIt may stay in the buffer, the file looks empty
new FileWriter(f) appendsIt erases; to append, the second parameter is true
try-with-resources closes resources in the opening orderIn reverse order, before the catch
Any class can be used in try-with-resourcesAutoCloseable is required
readObject() returns a Student directlyIt returns Object, a cast is needed
readObject throws only IOExceptionClassNotFoundException too
A transient field gets its old value back when readThe default value: null, 0, false
A static field is saved with the objectIt is not saved, it belongs to the class
Every object inside a Serializable class is written automaticallyThe classes of those objects must be Serializable too
If hasNextInt is false the loop moves on by itselfIf you do not skip the token with next(), infinite loop

Test yourself

Exam-style questions. Solve them on paper first, then reveal the answer.

Question 1easy

Find the compile error.

public static void main(String[] args) {
    Scanner in = new Scanner(new File("data.txt"));
    System.out.println(in.nextLine());
}
Show answer
FileNotFoundException is checked, neither caught nor declared

new Scanner(File) can throw FileNotFoundException if the file does not exist, and that is a checked exception. javac: unreported exception FileNotFoundException; must be caught or declared to be thrown. Fix: main(...) throws FileNotFoundException or try/catch. Since the resource is not closed, try-with-resources is best.

Question 2medium

What is printed?

try (PrintWriter out = new PrintWriter("scores.txt")) {
    out.println("Ece 85");
    out.println("Mert 70");
    out.printf("%s %d%n", "Can", 92);
}
int total = 0, n = 0;
try (Scanner in = new Scanner(new File("scores.txt"))) {
    while (in.hasNext()) {
        String name = in.next();
        total += in.nextInt();
        n++;
    }
}
System.out.println("avg=" + (double) total / n);
Show answer
avg=82.33333333333333

Three lines are written to the file; when the try-with-resources block ends, the file is closed and written to disk. On each line Scanner reads first the name (next), then the number (nextInt): 85 + 70 + 92 = 247, n = 3. The (double) cast is applied before the division: 247 / 3.0 = 82.33333333333333.

Question 3hard

What is printed?

class Res implements AutoCloseable {
    private final String name;
    Res(String name) { this.name = name; System.out.println("open " + name); }
    public void close() { System.out.println("close " + name); }
}
// in main:
try (Res a = new Res("A"); Res b = new Res("B")) {
    System.out.println("body");
    throw new IllegalStateException("boom");
} catch (IllegalStateException e) {
    System.out.println("caught " + e.getMessage());
} finally {
    System.out.println("finally");
}
Show answer
open A / open B / body / close B / close A / caught boom / finally

Resources are opened in the order written and closed in REVERSE order (B first). Closing happens BEFORE the catch: when the exception is thrown, first the resources are closed, then the catch runs, and finally last.

Question 4medium

What is printed?

class Student implements Serializable {
    String name; double gpa;
    transient String password;
    Student(String n, double g, String p) { name = n; gpa = g; password = p; }
    public String toString() { return name + " " + gpa + " " + password; }
}
// write new Student("Deniz", 3.4, "secret") with ObjectOutputStream,
// then read it back with ObjectInputStream and print it
Show answer
Deniz 3.4 null

transient fields are not serialized. When the object is read back, that field gets its default value: null for String. It is used for data that should not be saved, such as a password. static fields are not serialized either; they belong to the class, not to the object.

Question 5medium

What happens?

class Address { String city = "Istanbul"; }
class Person implements Serializable {
    String name = "Ali";
    Address address = new Address();
}
// oos.writeObject(new Person());
Show answer
NotSerializableException at run time (Address is not Serializable)

Serialization writes the object together with all the objects it holds (object graph). Person is Serializable but the Address inside it is not: writeObject throws NotSerializableException, and its message is the name of the problem class. It is not a compile error; the compiler does not check this. Fix: make Address implements Serializable too, or make the field transient.

Question 6medium

A PrintWriter writes hello to a file but close() (or flush()) is never called. What is the file size right after println, before closing?

Show answer
0 bytes

PrintWriter (over a FileWriter) first writes the data to an in-memory buffer; it goes to disk only with flush or close. In the test run Files.size gave 0 after println and was greater than 0 after close. If the program ends without closing, the data can be lost. That is why try-with-resources matters.

Question 7medium

What does the file contain after these three blocks?

try (PrintWriter w = new PrintWriter(new FileWriter("log.txt"))) { w.println("first"); }
try (PrintWriter w = new PrintWriter(new FileWriter("log.txt"))) { w.println("second"); }
try (PrintWriter w = new PrintWriter(new FileWriter("log.txt", true))) { w.println("third"); }
Show answer
second, third (two lines)

new FileWriter(file) writes the file from scratch: the second block erases "first". new FileWriter(file, true) is append mode: "third" is added. The result is [second, third].

Question 8medium

What is printed?

// mixed.txt contains:  3 x 4
//                      5.5 6 end
int sum = 0;
try (Scanner in = new Scanner(Paths.get("mixed.txt"))) {
    while (in.hasNext()) {
        if (in.hasNextInt()) sum += in.nextInt();
        else in.next();
    }
}
System.out.println(sum);
Show answer
13

Scanner splits into tokens by whitespace and line ends: 3, x, 4, 5.5, 6, end. hasNextInt is true only for integers: 3 + 4 + 6 = 13. Skipping the other tokens with next() is a must; if you do not skip, the loop spins forever at x.

Question 9hard

What are the two lines of report.txt?

try (Formatter f = new Formatter("report.txt")) {
    f.format("%-6s|%5.1f|%03d%n", "Ada", 3.456, 7);
    f.format("%-6s|%5.1f|%03d%n", "Bora", 12.0, 42);
}
Show answer
Ada | 3.5|007 and Bora | 12.0|042

%-6s writes left-aligned in a field of 6 characters (Ada + 3 spaces). %5.1f is a width of 5, 1 decimal, right-aligned: " 3.5" (3.456 was rounded), " 12.0". %03d is 3 digits, padded with zeros: 007, 042. %n is the line end.

Question 10hard

Find the compile error.

static Object load(ObjectInputStream ois) throws IOException {
    return ois.readObject();
}
Show answer
readObject also throws ClassNotFoundException, which is not declared

readObject can throw two checked exceptions: IOException and ClassNotFoundException (if the class being read is not found). javac: unreported exception ClassNotFoundException; must be caught or declared to be thrown. Fix: throws IOException, ClassNotFoundException.

Question 11easy

Which statement about object serialization is TRUE?

  1. static fields are saved with each object
  2. transient fields are restored with their original values
  3. A class must implement Serializable to be written with writeObject
  4. readObject returns the exact same object (==) that was written
Show answer
A class must implement Serializable to be written with writeObject

Serializable is a marker interface: it has no methods but it is required. static fields are not written (in the test run, a static field changed after writing stayed changed after reading too). transient fields come back with the default value. readObject creates a new object: back == names is false, back.equals(names) is true.

Question 12hard

Write a method double averageOfFile(Path path) throws IOException that reads all tokens of a text file and returns the average of those that are numbers (skip the others). Return 0 if there are no numbers. Use try-with-resources.

Show answer
try (Scanner in = new Scanner(path)) + hasNextDouble / next

Open the Scanner inside try-with-resources; it is closed even if there is an exception. Skip the tokens that are not numbers with next():

public static double averageOfFile(Path path) throws IOException {
    int count = 0;
    double sum = 0;
    try (Scanner in = new Scanner(path)) {
        while (in.hasNext()) {
            if (in.hasNextDouble()) {
                sum += in.nextDouble();
                count++;
            } else {
                in.next();
            }
        }
    }
    return count == 0 ? 0 : sum / count;
}
For the file "3 x 4 / 5.5 6 end", (3 + 4 + 5.5 + 6) / 4 = 4.625. Note: on a computer with a Turkish locale, Scanner expects a comma as the decimal separator, does not count "5.5" as a number, and the result becomes (3 + 4 + 6) / 3 = 4.3333. If needed, in.useLocale(Locale.US);.

Verification: All code examples were run with JDK 26 by creating real files in a temporary folder (java verify/SEN1002/W11.java); the behavior under the Turkish locale was tried with java -Duser.language=tr -Duser.country=TR -Duser.format=tr W11.java. Compile errors: verify/SEN1002/err/E11a-c.java.

Review card

Text file: humans read it (Scanner/PrintWriter). Binary: Java reads it (ObjectStream). Path p = Paths.get("data", "a.txt"); Files.exists(p) Files.size(p) Files.isDirectory(p) Files.readAllLines(p) → List<String>. Files.write(p, list). Files.writeString(p, s). Reading: new Scanner(new File("a.txt")) or new Scanner(path) → FileNotFoundException/IOException CHECKED. Scanner: hasNext/next, hasNextInt/nextInt, nextLine. If it is not a number, SKIP with next(). Writing: new PrintWriter("a.txt") → println, printf. Formatter → format("%-6s%5.1f%n", ...). new FileWriter(f) ERASES the file, new FileWriter(f, true) APPENDS. Without close/flush the data stays in the buffer, the file may be empty. try (A a = ...; B b = ...) { } → automatic close, in REVERSE order (b then a), BEFORE the catch. try-with-resources requires AutoCloseable. An error during close → suppressed. Serialization: class X implements Serializable; the objects inside must be Serializable too. ObjectOutputStream.writeObject(o) / (Type) ObjectInputStream.readObject() → cast REQUIRED. readObject: IOException + ClassNotFoundException. End of file: EOFException. transient and static fields are not written; a transient field comes back as the default value (null, 0).