RizTech Academy logo
RizTech Academy
Files, Dates and DataLesson 2 of 425 min

Reading and writing text and CSV

CSV is the format your data will actually arrive in. Not JSON, not a database export — a spreadsheet somebody saved, emailed, and expects your program to read.

It is also deceptively hard, and this lesson's real purpose is to show you exactly where a hand-written parser stops working, so you can decide honestly whether to keep going or reach for a library.

Reading rows, and collecting failures

The shape from module 7, applied to a file:

int line = 0;
List<Row> rows = new ArrayList<>();
List<String> problems = new ArrayList<>();

for (String text : Files.readAllLines(file, StandardCharsets.UTF_8)) {
    line++;
    if (line == 1 || text.isBlank()) continue;      // header, blanks

    String[] parts = text.split(",", -1);
    if (parts.length != 4) {
        problems.add("line %d: expected 4 fields, found %d".formatted(line, parts.length));
        continue;
    }
    try {
        rows.add(new Row(LocalDate.parse(parts[0]), parts[1], parts[2], Integer.parseInt(parts[3])));
    } catch (RuntimeException e) {
        problems.add("line %d: %s".formatted(line, e.getMessage()));
    }
}
parsed 3, rejected 2
  line 4: expected 4 fields, found 5
  line 5: For input string: "notanumber"

Four details doing real work:

split(",", -1) keeps trailing empty fields, as the strings lesson warned. A missing final column is data, not absence.

The field count is checked before the fields are used. Otherwise a short row gives ArrayIndexOutOfBoundsException, which says nothing about which line.

The try is inside the loop, so one bad row does not stop the other three thousand.

The line number is in every message. Without it the report is useless on a large file.

Where the naive split breaks

Look at line 4 of the input:

2026-09-02,"Kale, Kavita",Wagholi,3
line 4: expected 4 fields, found 5

The customer's name contains a comma, correctly quoted as CSV requires — and split(",") knows nothing about quotes. This is not an edge case. Indian names written surname-first, addresses, and anything a human typed will contain commas.

This is the moment to decide. Either use a library, or write a real field splitter. Here is the minimal correct one, so you know what it costs:

static String[] splitCsv(String line) {
    List<String> out = new ArrayList<>();
    StringBuilder current = new StringBuilder();
    boolean inQuotes = false;

    for (int i = 0; i < line.length(); i++) {
        char c = line.charAt(i);
        if (inQuotes) {
            if (c == '"') {
                if (i + 1 < line.length() && line.charAt(i + 1) == '"') {
                    current.append('"');    // "" inside quotes is a literal quote
                    i++;
                } else {
                    inQuotes = false;
                }
            } else {
                current.append(c);
            }
        } else if (c == '"') {
            inQuotes = true;
        } else if (c == ',') {
            out.add(current.toString());
            current.setLength(0);
        } else {
            current.append(c);
        }
    }
    out.add(current.toString());
    return out.toArray(new String[0]);
}
2026-09-02,"Kale, Kavita",Wagholi,3  ->  [2026-09-02, Kale, Kavita, Wagholi, 3]
a,"b""c",d                           ->  [a, b"c, d]
a,,c                                 ->  [a, , c]

Twenty lines, and it still does not handle a newline inside a quoted field — which is legal CSV and which Excel produces whenever a cell contains a line break. Handling that means abandoning line-at-a-time reading entirely and tokenising the whole stream.

That is the honest boundary. Quoted commas are worth twenty lines. Embedded newlines are not: use a library. Apache Commons CSV and OpenCSV both do the whole of RFC 4180 and neither is a large dependency. The next lesson shows how to add one; module 10 shows the tooling.

This course writes the splitter by hand because knowing why the library exists is worth more than knowing its API. A developer who has never hit the quoted comma will write split(",") in production.

Writing CSV

Writing needs the same quoting rules in reverse:

static String quote(String field) {
    if (field.contains(",") || field.contains("\"") || field.contains("\n")) {
        return '"' + field.replace("\"", "\"\"") + '"';
    }
    return field;
}
try (BufferedWriter w = Files.newBufferedWriter(out, StandardCharsets.UTF_8)) {
    w.write("date,customer,area,tiffins");
    w.newLine();
    for (Row r : rows) {
        w.write(String.join(",",
                r.date().toString(), quote(r.customer()), quote(r.area()), String.valueOf(r.tiffins())));
        w.newLine();
    }
}
date,customer,area,tiffins
2026-09-01,Priya Deshmukh,Wagholi,2
2026-09-01,Arjun Kale,Kharadi,1
2026-09-03,Rahul,,2

Writing CSV without quoting is the bug that creates the file the next program cannot read. One customer with a comma in their name and every downstream consumer gets a shifted column — silently, because the row still parses.

w.newLine() rather than \n writes the platform's line separator. For a file another system will read, \n is often the safer choice; for a file a Windows user will open in Notepad, newLine() is kinder. Decide, do not default.

Which reading method

Method Use when
Files.readString(p) You want the whole thing as one string
Files.readAllLines(p) The file fits in memory and you want a List
Files.lines(p) The file is large — close it
Files.newBufferedReader(p) You need fine control, or a readLine loop
Files.readAllBytes(p) It is not text
try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) {
    long n = lines.skip(1).filter(s -> !s.isBlank()).count();
}
data lines: 5

And for writing:

Method Use when
Files.writeString(p, s) You have the whole content
Files.write(p, lines) You have a List<String>
Files.newBufferedWriter(p) Writing incrementally, or a large file

Use a BufferedWriter for anything in a loop. Writing line by line with Files.writeString and APPEND reopens the file every time, which is thousands of system calls where one would do.

Two traps worth naming

The byte order mark. A file saved as "UTF-8 with BOM" by Excel or Notepad begins with an invisible character, so your first header becomes \uFEFFdate rather than date and every lookup by column name fails. Nothing looks wrong on screen. Strip it: if the first character of the file is \uFEFF, drop it.

The header row. Do not assume the column order. Read the header, build a Map<String, Integer> of name to index, and look fields up by name. A spreadsheet somebody re-saved with columns reordered will otherwise load silently wrong, which is worse than failing.

Check your work

Why split(",", -1)? Without the limit, trailing empty fields are discarded, so a missing last column silently disappears.

Why check the field count before using the fields? A short row otherwise throws ArrayIndexOutOfBoundsException, which does not say which line or which column.

Where does the try go? Inside the loop, so one bad row does not abandon the rest of the file.

What breaks split(",") on real data? A comma inside a quoted field — "Kale, Kavita". It produces five fields instead of four.

What does a hand-written splitter still not handle? A newline inside a quoted field, which is legal CSV and which Excel produces routinely. That needs tokenising the whole stream, and is the point to use a library.

What happens if you write CSV without quoting? A field containing a comma shifts every column after it, and the row still parses — so the corruption is silent.

When should you use BufferedWriter over Files.writeString? Whenever you are writing in a loop. writeString with APPEND reopens the file each time.

Why look columns up by header name rather than index? A re-saved spreadsheet with reordered columns otherwise loads silently wrong.

Practice 2, the quoted comma. "Kale, Kavita" gives line 4: expected 4 fields, found 5. Adding the quote-aware splitter fixes it and gives [2026-09-02, Kale, Kavita, Wagholi, 3] — five printed elements because the name legitimately contains a comma, but four fields. Test a,"b""c",d too: the doubled quote is an escaped quote and should give [a, b"c, d].

Practice 5, the round trip. Writing without quote() and reading back gives more fields than you wrote, with the name split across two columns and every later column shifted. Nothing throws. With quote() the round trip is exact. That silence is why the check belongs in the writer, not only the reader.

Practice

  1. Read a CSV, collect failures. Five rows, two of them broken in different ways. Report every problem with its line number, and report how many rows loaded.

  2. Break it with a comma. Add a row with a quoted name containing a comma. Watch the field count fail. Then write the quote-aware splitter and confirm it parses. Test a,"b""c",d and a,,c as well.

  3. Find the limit. Add a row with a newline inside a quoted field. Work out what your splitter would need to handle it, then write one paragraph deciding whether you would write that or add a dependency.

  4. Look up by header. Read the header row into a Map<String, Integer> and fetch fields by name. Then reorder the columns in the file and confirm your program still works.

  5. Round trip. Write your rows out, read them back, and assert the results are equal. Then remove the quoting from the writer and do it again.

  6. Harder — a BOM and a blank line. Save a CSV with a UTF-8 BOM (in VS Code, the encoding selector at the bottom right) and a blank line in the middle. Make your reader handle both: strip the BOM if present, skip blank lines, and still report accurate line numbers for the rows that follow. The line numbers are the part people get wrong.

Next: dates and times, which is the other half of every row you just parsed.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship