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
-
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.
-
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",danda,,cas well. -
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.
-
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. -
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.
-
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