انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-034-L02

قراءة النصوص وكتابتها

  • الدرس 2 من 6
  • 10 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

Lesson Overview

You can now point at a file. This lesson puts text into it and gets text back out, using four one-line methods of the Files class: readAllLines, readString, writeString, and write. For small text files such as notes, settings, and a contact list, these are all you need.

The filing-cabinet picture continues: the path is the label on the folder, and these methods are the clerk who takes a whole page out, or files a whole page in, in one move. Two details decide whether your data survives. First, by default writing replaces what was in the file. Second, every one of these methods can throw IOException, so you must decide what your program does when the disk says no.

Why this matters for a backend: configuration files, exported reports, and simple logs are all text files. Knowing exactly what happens to an existing file (replaced, or added to) is the difference between a log that grows and a log that silently loses its history.

Previously, "Paths and the Files API" showed how to build paths and create folders. The next lesson, "Streams, readers, and buffering", covers what to do when a file is too big to load in one go.

Learning Objectives

By the end of this lesson you will be able to:

  • Read a whole text file as a List<String> with Files.readAllLines and as one String with Files.readString.
  • Write text with Files.writeString and a list of lines with Files.write.
  • Explain that writing replaces the file by default, and append with StandardOpenOption.APPEND.
  • Explain which method adds line breaks and which does not.
  • Handle IOException, including a file that does not exist yet.
  • Say why these methods suit small files only.

Written Explanation

Reading

java
List<String> lines = Files.readAllLines(path);
String text = Files.readString(path);

readAllLines returns every line as an element of a List<String>, without the line breaks. readString returns the entire content as one String, line breaks included. Pick readAllLines when you will process line by line (the common case) and readString when you want the text as a whole.

Both methods open the file, read it, and close it for you, even if something goes wrong. You do not need try-with-resources for them. Both decode the bytes as UTF-8; the encoding lesson explains why that matters.

If the file does not exist, they throw an IOException (usually NoSuchFileException). A missing file is not a bug in your program. It is a normal situation, for example on the very first run, so plan for it.

Writing

java
Files.writeString(path, "hello\n");
Files.write(path, List.of("a", "b", "c"));

writeString writes the exact characters you give it. It adds nothing, so a line break exists only if you type \n yourself. write(path, lines) takes any collection of strings and ends each element with a line separator, so you get one line per element.

By default both methods behave as if three options were given: create the file if needed, write to it, and truncate it to zero length first. In plain words: if the file already exists, its old content is gone. Run writeString twice and only the second text remains.

Appending

To add to the end of a file instead, pass options:

java
Files.writeString(path, line + "\n",
        StandardOpenOption.CREATE, StandardOpenOption.APPEND);

APPEND says "add at the end". CREATE says "make the file if it is missing". You usually want both: with APPEND alone, a missing file causes a NoSuchFileException. This is the pattern for logs.

Handling IOException

Every method above declares IOException, a checked exception. The compiler forces you to either catch it or declare throws IOException. Remember the exceptions module: catch where you can do something useful. For a missing data file, that often means "start with an empty list":

java
try {
    List<String> lines = Files.readAllLines(file);
} catch (IOException e) {
    System.out.println("File problem: " + e.getMessage());
}

Print a clear message that names the file. A bare e.printStackTrace() or an empty catch block hides the real problem.

Small files only

readAllLines and readString load the whole file into memory. The documentation says they are intended for small files, and that reading a very large file this way can run out of memory. A few thousand contact lines are fine. A multi-gigabyte log file is not. For those, the next lesson shows how to read one line at a time.

Examples

Example 1: write, replace, append lines, read back (simple)

java
import java.io.IOException;
import java.nio.file.*;
import java.util.List;

public class Ex1 {
    public static void main(String[] args) throws IOException {
        Path file = Path.of("data", "note.txt");
        Files.createDirectories(file.getParent());
        Files.writeString(file, "first\n");
        Files.writeString(file, "second\n");          // replaces
        System.out.print(Files.readString(file));
        Files.write(file, List.of("a", "b", "c"));     // each element becomes a line
        List<String> lines = Files.readAllLines(file);
        System.out.println(lines + " size " + lines.size());
        Files.writeString(file, "x");                  // no newline added
        System.out.println(Files.readAllLines(file));
    }
}

Output:

text
second
[a, b, c] size 3
[x]

The first text, first, is gone: the second write replaced it. The list of three elements became three lines, and readAllLines gave them back as three elements without any line-break characters. Even x with no newline at the end counts as one line.

Example 2: a simple log with APPEND (practical)

java
import java.io.IOException;
import java.nio.file.*;

public class Ex2 {
    static void log(Path file, String msg) throws IOException {
        Files.writeString(file, msg + "\n",
                StandardOpenOption.CREATE, StandardOpenOption.APPEND);
    }

    public static void main(String[] args) throws IOException {
        Path file = Path.of("data", "app.log");
        Files.createDirectories(file.getParent());
        Files.deleteIfExists(file);
        log(file, "started");
        log(file, "working");
        log(file, "stopped");
        System.out.println(Files.readAllLines(file));
        try {
            Files.writeString(Path.of("data", "missing.log"), "x", StandardOpenOption.APPEND);
        } catch (IOException e) {
            System.out.println(e.getClass().getSimpleName());
        }
    }
}

Output:

text
[started, working, stopped]
NoSuchFileException

Each call adds a line and keeps the earlier ones. The last lines prove the point about options: APPEND without CREATE does not make a missing file, so the call throws. (The deletion at the start only makes the demo repeatable.)

Example 3: loading a file that may not exist (realistic)

java
import java.io.IOException;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;

public class Ex3 {
    static List<String> loadLines(Path file) {
        if (!Files.exists(file)) {
            return new ArrayList<>();
        }
        try {
            return Files.readAllLines(file);
        } catch (IOException e) {
            System.out.println("Could not read " + file + ": " + e.getMessage());
            return new ArrayList<>();
        }
    }

    public static void main(String[] args) throws IOException {
        Path file = Path.of("data", "todo.txt");
        Files.deleteIfExists(file);
        System.out.println(loadLines(file));
        Files.createDirectories(file.getParent());
        Files.write(file, List.of("buy milk", "", "call Ana"));
        List<String> lines = loadLines(file);
        for (int i = 0; i < lines.size(); i++) {
            System.out.println((i + 1) + ": " + lines.get(i));
        }
    }
}

Output:

text
[]
1: buy milk
2: 
3: call Ana

On a first run the file is missing, so the method returns an empty list and the program carries on. If the file exists but cannot be read, it reports which file failed and still returns a usable empty list. Notice that the blank line is kept as an empty string, which is why line 2 is empty. This "missing means empty" pattern is exactly what the module's lab needs.

Backend Insight

Small text files are the glue of many backend systems: an application reads its settings at startup, a batch job writes a report for another team, a service appends to a log. The decision each time is the same one you just practised: replace, or append?

A frequent production bug is writing a file with the default "replace" behaviour when the intent was to add. The service restarts, and last week's entries are gone. The reverse bug exists too: appending to a file that should have been rewritten, so it grows forever. State your intent in the code with explicit options, and say it in a comment when it is not obvious.

Also think about size. A function that calls readAllLines on an upload is fine in a test with ten lines and dangerous in production with a user who sends a huge file. Later modules on file uploads return to this problem.

Common Mistakes

Mistake 1: Expecting writeString to add a new line

  • Incorrect: Files.writeString(file, "one"); Files.writeString(file, "two"); and expecting two lines.
  • Correct: the second call replaces the first, and neither adds a line break. Use APPEND and add "\n" yourself, or build a list and call Files.write(file, lines) once.

Mistake 2: Overwriting a file by accident

Calling Files.write(path, lines) on a file that already holds data destroys the old data. Before you write, ask: should the old content survive? If yes, append, or read it first and write the merged result.

Mistake 3: Using APPEND without CREATE

APPEND alone fails on the very first run because the file does not exist yet. Pass StandardOpenOption.CREATE too.

Mistake 4: Swallowing the exception

java
try { Files.readAllLines(file); } catch (IOException e) { }   // wrong

An empty catch hides the reason the data is missing. Report the file name and message, or rethrow.

Mistake 5: Loading a huge file into memory

readAllLines and readString are for small files. For large ones, read line by line (next lesson).

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. Files.writeString(p, "A\n"); runs, then Files.writeString(p, "B\n");. What is in the file afterwards?

أظهر الحل A1

A1. Only B. The second write truncates the file first.

A2. Which method returns a List<String>, and which returns one String: readAllLines or readString?

أظهر الحل A2

A2. readAllLines returns the List<String>; readString returns one String.

A3. Files.write(p, List.of("x", "y")) and Files.writeString(p, "x" + "y"). How many lines does each produce?

أظهر الحل A3

A3. The first produces two lines (x and y). The second writes xy: one line, because writeString adds nothing.

Level B: Application

B1. Write static void appendLine(Path f, String line) throws IOException that adds a line to the end of a file and creates the file if needed.

أظهر الحل B1

B1.

java
static void appendLine(Path f, String line) throws IOException {
    Files.writeString(f, line + System.lineSeparator(),
            StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}

B2. Write static long countNonBlank(Path f) throws IOException that returns the number of lines that are not blank. (String.isBlank() tells you whether a string is empty or only spaces.)

أظهر الحل B2

B2.

java
static long countNonBlank(Path f) throws IOException {
    long n = 0;
    for (String l : Files.readAllLines(f)) {
        if (!l.isBlank()) n++;
    }
    return n;
}

For a file with the lines one, an empty line, and three, it returns 2 (run and verified).

B3. The file data/config.txt may not exist. Write code that prints Using defaults when it is missing and otherwise prints each line with its line number. Do not let the program crash.

أظهر الحل B3

B3.

java
Path cfg = Path.of("data", "config.txt");
if (!Files.exists(cfg)) {
    System.out.println("Using defaults");
} else {
    try {
        List<String> lines = Files.readAllLines(cfg);
        for (int i = 0; i < lines.size(); i++) {
            System.out.println((i + 1) + ": " + lines.get(i));
        }
    } catch (IOException e) {
        System.out.println("Could not read " + cfg + ": " + e.getMessage());
    }
}

Level C: Challenge

C1. Write static void removeLine(Path f, int number) throws IOException that deletes the line with the given 1-based number from the file. If the number is out of range, print No line N and leave the file unchanged. Hint: the documentation does not promise that the returned list can be modified, so copy it into an ArrayList first.

أظهر الحل C1

C1.

java
static void removeLine(Path f, int number) throws IOException {
    List<String> lines = new ArrayList<>(Files.readAllLines(f));
    if (number < 1 || number > lines.size()) {
        System.out.println("No line " + number);
        return;
    }
    lines.remove(number - 1);
    Files.write(f, lines);
}

Starting from one, a blank line, three: removeLine(f, 2) left [one, three], and removeLine(f, 9) printed No line 9 (run and verified). The code needs java.util.* imports.

C2. A teammate's log shows only today's entries, although the program has run for a week. The code is Files.writeString(log, entry + "\n");. Explain the cause and fix it.

أظهر الحل C2

C2. The default behaviour of writeString is to truncate the file, so each run (or each entry) replaces the previous content. Fix: Files.writeString(log, entry + "\n", StandardOpenOption.CREATE, StandardOpenOption.APPEND);.

Mini Challenge

Build a tiny to-do program with a file data/todo.txt. Write three methods: List<String> load() (an empty list if the file is missing), void add(String task) (adds one line, keeping old ones), and void show() (prints numbered tasks, or Nothing to do for an empty list). Run the program twice with different add calls and check that the tasks from the first run are still there.

Check your work: the first run works without the file existing, the second run sees the old tasks, no catch block is empty, and every path is relative.

Summary & Next Lesson

Key takeaways

  • readAllLines gives a List<String>; readString gives one String. Both close the file for you.
  • writeString writes exactly what you give it; write(path, lines) ends each line with a line separator.
  • By default, writing replaces an existing file. Use CREATE and APPEND to add to the end.
  • All of these methods throw IOException. A missing file is a normal case to plan for.
  • They load or write everything at once, so use them only for small files.

Next lesson

The next lesson, "Streams, readers, and buffering", shows how to process big files one line at a time and introduces the difference between byte streams and character streams.

Self-check

  1. What does Files.writeString do to an existing file by default?
  2. Which options make a write add to the end of a file and create the file if missing?
  3. Does readAllLines keep the line break characters in each element?
  4. Why is an empty catch (IOException e) { } a bad idea?
  5. Why are readAllLines and readString unsuitable for huge files?
أظهر الإجابات
  1. It replaces the content: the file is truncated first, then the new text is written.
  2. StandardOpenOption.CREATE and StandardOpenOption.APPEND.
  3. No. Each element is the line without its line terminator.
  4. It hides the failure, so you cannot tell why data is missing. Report the file and message, or rethrow.
  5. They load the entire file into memory, which can run out of memory for very large files.

References

  • Oracle, Files (Java SE 21 API). It supports the UTF-8 default of these methods, the default write options (create, truncate, write), that they close the file themselves, that write ends each line with the platform line separator while writeString adds none, and the small-file warning.
  • Behaviour shown in the example outputs (such as NoSuchFileException for APPEND on a missing file) was confirmed by compiling and running the code with JDK 21.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.