A simple CSV format
On This Lesson 11 topics
Lesson Overview
You can now read and write text lines, and you know how to avoid garbled characters. The next question is what to put in those lines. A contact has a name, a phone number, and an email: three pieces of data. A file is just text, so you need a convention for turning an object into one line of text and back again.
This lesson introduces CSV, short for comma-separated values: one record per line, fields separated by commas. It is the simplest format that a spreadsheet, a database export, and your own program can all understand. In the filing-cabinet picture, one line is one index card with several boxes, and the commas are the lines that separate the boxes.
Why this matters for a backend: importing and exporting data as CSV is an everyday task, and "the import failed on line 4" is a message every developer has written. You will build the simple version by hand and learn exactly where it breaks, so that you know when to reach for a proper library later.
The previous lesson, "Character encoding (UTF-8)", made sure the text survives. The next lesson, "Demo: saving and loading contacts as CSV", assembles everything into the module's lab.
Learning Objectives
By the end of this lesson you will be able to:
- Describe the CSV layout: one record per line, fields separated by commas.
- Write a record as a line with
String.join(",", ...). - Read a line back with
line.split(",", -1)and explain why the-1matters. - Validate each line by checking the number of fields, and report bad lines instead of crashing.
- Explain why a comma inside a field breaks this simple format and what real CSV does about it.
- Convert between a
Contactrecord and a CSV line in both directions.
Written Explanation
The layout
A CSV file for contacts looks like this:
Ana,0501112222,ana@example.com
Omar,0503334444,Each line is one contact. The commas separate the fields: name, phone, email. Omar has no email, so his last field is empty: the line ends with a comma and nothing after it. The empty field is still a field. Keeping that in mind prevents the most common parsing bug.
The format has no official single standard. RFC 4180 describes the common practice and is labelled informational, not a formal standard. It confirms the basics used here: records on separate lines, commas between fields, an optional header line, and the same number of fields on each line. Some files start with a header line (name,phone,email) naming the columns. Ours will not, to keep it simple, but a loader should be ready to skip one if you add it.
Writing a line
String.join glues pieces together with a separator:
String line = String.join(",", name, phone, email);An empty string for the email gives Omar,0503334444, with the final comma, which is exactly right. Never write null into a field: String.join would produce the text null. Store the empty string for "no value".
Reading a line
String.split cuts a string at every match of a separator. The version with one argument has a trap:
"Omar,0503334444,".split(",").length // 2: the trailing empty field is dropped
"Omar,0503334444,".split(",", -1).length // 3: keptBy default split discards trailing empty strings. A negative limit tells it to keep them. So always write split(",", -1) when you parse records, then check that you got the number of fields you expect. (Careful: the comma here is treated as a pattern. It is safe for a comma, but characters such as | or . have special meaning in patterns and would need escaping. We stay with commas.)
Spaces count as part of a field's value, so strip() each field if you want to ignore accidental spaces. A blank line gives one field (the empty string) from split, so it fails the field-count check. Decide explicitly: either skip blank lines on purpose, or treat them as bad.
Validating: bad lines happen
Files get edited by hand, truncated by crashes, or produced by a different program. A loader must expect lines that are wrong and must not crash on them. The validation in this lesson is small:
- The line must have exactly three fields.
- The name must not be empty.
When a line is wrong, throw an IllegalArgumentException from the parsing method with a clear message. The loop that reads the file catches it, counts the bad line, prints its line number and reason, and carries on with the next line. That is the design the module's lab asks for: bad lines reported, not fatal.
Where the simple format breaks
What if a name contains a comma, such as Smith, John? The line becomes Smith, John,0505556666,j@example.com, and split finds four fields instead of three. Real CSV solves this by wrapping such a field in double quotes, and doubling any quote character inside it (also described in RFC 4180). Parsing quotes correctly is a job for a library, which you will use in later work. Writing your own quote handling is full of edge cases, such as a field that contains a line break.
For this module we keep the simple format and accept the limit: our parser rejects a line with the wrong number of fields rather than silently loading wrong data. A cheap workaround is to forbid commas in input or replace them with another character when saving, but that changes the user's data, so treat it as a stopgap.
Examples
Example 1: join and split (simple)
import java.util.Arrays;
public class Ex1 {
public static void main(String[] args) {
String line = String.join(",", "Ana", "0501112222", "ana@example.com");
System.out.println(line);
String[] parts = line.split(",");
System.out.println(parts.length + " " + Arrays.toString(parts));
String omar = "Omar,0503334444,";
System.out.println(omar.split(",").length + " vs " + omar.split(",", -1).length);
System.out.println(Arrays.toString(omar.split(",", -1)));
String comma = String.join(",", "Smith, John", "0505556666", "j@example.com");
System.out.println(comma.split(",", -1).length);
}
}Output:
Ana,0501112222,ana@example.com
3 [Ana, 0501112222, ana@example.com]
2 vs 3
[Omar, 0503334444, ]
4The -1 version keeps Omar's empty email as the third element. The last line shows the comma problem: a name with a comma yields four fields instead of three.
Example 2: a Contact that converts itself (practical)
public class Ex2 {
record Contact(String name, String phone, String email) {
String toCsv() {
return String.join(",", name, phone, email);
}
static Contact fromCsv(String line) {
String[] f = line.split(",", -1);
if (f.length != 3) {
throw new IllegalArgumentException("expected 3 fields but found " + f.length);
}
String name = f[0].strip();
if (name.isEmpty()) {
throw new IllegalArgumentException("name is empty");
}
return new Contact(name, f[1].strip(), f[2].strip());
}
}
public static void main(String[] args) {
Contact c = new Contact("Ana", "0501112222", "ana@example.com");
System.out.println(c.toCsv());
System.out.println(Contact.fromCsv(c.toCsv()).equals(c));
System.out.println(Contact.fromCsv("Omar,0503334444,"));
for (String bad : new String[] {"Lee,0501", ",0501,x@y", "Smith, John,0505,j@x"}) {
try {
Contact.fromCsv(bad);
} catch (IllegalArgumentException e) {
System.out.println("bad line [" + bad + "]: " + e.getMessage());
}
}
}
}Output:
Ana,0501112222,ana@example.com
true
Contact[name=Omar, phone=0503334444, email=]
bad line [Lee,0501]: expected 3 fields but found 2
bad line [,0501,x@y]: name is empty
bad line [Smith, John,0505,j@x]: expected 3 fields but found 4A record (previewed in the earlier lesson on records) is used here to keep the example short; a regular class would work the same. The round trip, converting to a line and back, gives an equal object: that is the property a good file format must have. The three bad lines are rejected with a reason, including the comma-in-name case.
Example 3: load a file, skip and report bad lines (realistic)
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
public class Ex3 {
record Contact(String name, String phone, String email) {
static Contact fromCsv(String line) {
String[] f = line.split(",", -1);
if (f.length != 3) {
throw new IllegalArgumentException("expected 3 fields but found " + f.length);
}
return new Contact(f[0].strip(), f[1].strip(), f[2].strip());
}
}
public static void main(String[] args) throws IOException {
Path file = Path.of("data", "people.csv");
Files.createDirectories(file.getParent());
Files.write(file, List.of("Ana,0501112222,ana@example.com", "", "Omar,0503334444,",
"Broken line", "Lee,0507778888,lee@example.com"));
List<String> lines = Files.readAllLines(file);
List<Contact> ok = new ArrayList<>();
int bad = 0;
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
if (line.isBlank()) continue;
try {
ok.add(Contact.fromCsv(line));
} catch (IllegalArgumentException e) {
bad++;
System.out.println("Line " + (i + 1) + " skipped: " + e.getMessage());
}
}
System.out.println(ok.size() + " loaded, " + bad + " bad");
System.out.println(ok);
}
}Output:
Line 4 skipped: expected 3 fields but found 1
3 loaded, 1 bad
[Contact[name=Ana, phone=0501112222, email=ana@example.com], Contact[name=Omar, phone=0503334444, email=], Contact[name=Lee, phone=0507778888, email=lee@example.com]]The blank second line is skipped on purpose, the broken fourth line is reported with its number, and the other three contacts load. The program did not crash, and the user learns exactly which line to fix.
Backend Insight
CSV is still one of the most common ways for businesses to move data between systems: finance exports, customer imports, bulk uploads. A backend that accepts a CSV upload will meet every kind of bad input, so the shape of the code is always the same: parse line by line, validate, collect errors with line numbers, and report them together. Rejecting the whole file because of one bad line frustrates users, but silently skipping bad lines hides data loss, so good systems report the skipped lines.
Line numbers matter because a person has to fix the file. A message like "Line 4 skipped: expected 3 fields but found 1" can be acted on in seconds; "import failed" cannot.
Remember also that CSV has no types. Everything is text, and your code decides whether 0501112222 is a number or a string. Phone numbers are stored as text here for a reason: a leading zero would disappear if they were treated as integers.
Common Mistakes
Mistake 1: Using split(",") without -1
- Incorrect:
line.split(",")then readingparts[2], which fails withArrayIndexOutOfBoundsExceptionfor lines with an empty last field. - Correct:
line.split(",", -1)and checklength.
Mistake 2: Not validating the field count
Reading f[2] without checking f.length crashes on short lines. Validate first and throw a clear IllegalArgumentException.
Mistake 3: Forgetting that fields may contain commas
Data like Smith, John or an address breaks the line. For this simple format the right behaviour is to reject the line visibly, never to load half of a field.
Mistake 4: Writing null into a field
String.join turns a null into the text null. Store the empty string for "no value".
Mistake 5: Crashing on the first bad line
A try/catch around the whole loop stops loading at the first problem. Put the try/catch inside the loop, around the parse of one line, so every line gets its chance.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Write the single line that String.join(",", "Lee", "0507778888", "") produces.
Show solution A1
A1. Lee,0507778888, with a comma at the end and nothing after it.
A2. How many elements do "a,,c,".split(",") and "a,,c,".split(",", -1) return?
Show solution A2
A2. The first returns 3 elements (a, an empty string, c; the trailing empty string is discarded). The second returns 4 (a, empty, c, empty).
A3. Why does a name such as Smith, John break our CSV format?
Show solution A3
A3. The comma inside the name is read as a separator, so the line has four fields instead of three, and the data lands in the wrong boxes. Real CSV quotes such fields.
Level B: Application
B1. Write static int parseQty(String line) for lines like pen,12. It must throw IllegalArgumentException with a clear message if the line does not have exactly two fields, or if the second field is not a whole number.
Show solution B1
B1.
static int parseQty(String line) {
String[] f = line.split(",", -1);
if (f.length != 2) {
throw new IllegalArgumentException("expected 2 fields but found " + f.length);
}
try {
return Integer.parseInt(f[1].strip());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("quantity is not a number: " + f[1]);
}
}parseQty("pen,12") returned 12. "pen" gave expected 2 fields but found 1, and "pen,x" gave quantity is not a number: x (run and verified).
B2. Write static List<String> toLines(List<Contact> contacts) that returns one CSV line per contact, using Contact(name, phone, email) from the lesson.
Show solution B2
B2.
static List<String> toLines(List<Contact> contacts) {
List<String> out = new ArrayList<>();
for (Contact c : contacts) {
out.add(String.join(",", c.name(), c.phone(), c.email()));
}
return out;
}B3. What does " Ana , 050 ,a@b".split(",", -1) return, and what do you add to ignore the extra spaces?
Show solution B3
B3. It returns the three strings Ana, 050, and a@b, with the spaces still inside. Call strip() on each field to remove them.
Level C: Challenge
C1. Write static List<Contact> parseAll(List<String> lines) that skips a first line equal to name,phone,email (a header), skips lines with the wrong number of fields while printing Line N skipped: <line>, and returns the rest.
Show solution C1
C1.
static List<Contact> parseAll(List<String> lines) {
List<Contact> ok = new ArrayList<>();
int start = 0;
if (!lines.isEmpty() && lines.get(0).equals("name,phone,email")) {
start = 1;
}
for (int i = start; i < lines.size(); i++) {
String[] f = lines.get(i).split(",", -1);
if (f.length != 3) {
System.out.println("Line " + (i + 1) + " skipped: " + lines.get(i));
continue;
}
ok.add(new Contact(f[0], f[1], f[2]));
}
return ok;
}With the header, Ana,1,, oops, and Lee,2,l@x, it printed Line 3 skipped: oops and returned the two good contacts.
C2. A teammate "fixes" the comma problem by writing field.replace(",", ";") before saving. Name one advantage and one problem with that approach.
Show solution C2
C2. Advantage: lines never gain extra fields, so the format stays consistent. Problem: the data is changed. The name no longer matches what the user typed, and loading it back cannot restore the comma. Quoting, as in RFC 4180, keeps the original text.
Mini Challenge
Define a record Book(title, author, year), where year is an int. Give it toCsv() and a static fromCsv(String line) that validates three fields and that the year is a whole number between 1 and 2100, throwing IllegalArgumentException otherwise. Write a main that parses these lines, printing the good books and the line numbers of the bad ones: one valid line, one with a missing field, one with a non-numeric year, and one with an empty title.
Check your work: you used split(",", -1), the try/catch is inside the loop, a round trip fromCsv(b.toCsv()).equals(b) is true, and no input makes the program crash.
Summary & Next Lesson
Key takeaways
- CSV: one record per line, fields separated by commas. An empty field is still a field.
- Write with
String.join(",", ...). Read withsplit(",", -1)so trailing empty fields are kept. - Validate every line (field count, required fields). Report bad lines with their number instead of crashing.
- Put the
try/catchinside the loop, around one line, so one bad line does not stop the load. - A comma inside a field breaks the simple format. Real CSV quotes such fields; a library handles that.
Next lesson
The next lesson, "Demo: saving and loading contacts as CSV", combines paths, Files, UTF-8, and this CSV format into a save and load pair that survives a missing file and bad lines, which prepares the module's lab.
Self-check
- What does a CSV line for a contact with no email end with?
- Why do we call
split(",", -1)instead ofsplit(",")? - What should the loader do when a line has the wrong number of fields?
- Where should the
try/catchgo so that one bad line does not stop the whole load? - Why does a name with a comma break our parser?
Show answers
- A comma followed by nothing (the empty last field), for example
Omar,0503334444,. - Without
-1, trailing empty fields are discarded, so lines with an empty last field would look too short. - Report it (with its line number) and skip it, then continue with the next line.
- Inside the loop, around the parsing of a single line.
- The comma is read as a field separator, so the line has too many fields.
References
- IETF, RFC 4180: Common Format and MIME Type for CSV Files. It supports the layout (one record per line, comma-separated, optional header, equal field counts), quoting of fields that contain commas, and the doubling of inner quotes. The RFC is informational, not a formal standard.
- Oracle, String (Java SE 21 API). It supports
String.join, andsplitdiscarding trailing empty strings by default but keeping them with a negative limit (the page was only partly readable when checked; the negative-limit detail was confirmed by running the code on JDK 21). - The behaviour of
String.joinwith anullelement (it writes the textnull) was confirmed by running the code on JDK 21, not taken from the documentation.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.