Java's String.split() Trailing Empty Trap: The CSV Row That Loses Its Last Column

2026-07-16

This function ingests one row of a CSV export into a record keyed by header names. Header and row share the same comma-separated shape, so we split each and zip them together.

import java.util.HashMap;
import java.util.Map;

public class CsvIngest {
    public static Map<String, String> parseRow(String header, String row) {
        String[] cols = header.split(",");
        String[] vals = row.split(",");
        if (cols.length != vals.length) {
            throw new IllegalArgumentException(
                "column count mismatch: " + cols.length + " vs " + vals.length);
        }
        Map<String, String> record = new HashMap<>();
        for (int i = 0; i < cols.length; i++) {
            record.put(cols[i], vals[i]);
        }
        return record;
    }

    public static void main(String[] args) {
        String header = "id,name,email,notes";
        // notes field is empty for this user
        String row    = "42,Alice,[email protected],";
        System.out.println(parseRow(header, row));
    }
}

Every unit test passes: rows with populated notes fields round-trip fine. Then a batch of real production data hits and the ingester starts throwing column count mismatch: 4 vs 3 on rows that look identical to the header shape. Where did the fourth value go?

The Bug

The one-argument String.split(regex) is a convenience wrapper for split(regex, 0). And the 0 limit has a special behavior documented in a single easy-to-miss sentence: "trailing empty strings are therefore not included in the resulting array."

So "42,Alice,[email protected],".split(",") returns a length-3 array — ["42", "Alice", "[email protected]"] — because the empty string after the final comma is silently discarded. But the header "id,name,email,notes" has no trailing comma, so it splits into 4 elements. The two arrays no longer line up.

It gets worse. ",,,".split(",") returns an empty array, not four empty strings. Every trailing empty gets eaten until nothing is left. Any input that could ever end with empty fields is a landmine, and CSV, TSV, pipe-delimited exports, and log lines all frequently do.

The fix is to pass a negative limit, which disables the trailing-empty stripping and returns every token, empty or not:

String[] cols = header.split(",", -1);
String[] vals = row.split(",", -1);

Now "42,Alice,[email protected],".split(",", -1) returns the full four elements ["42", "Alice", "[email protected]", ""] and the record maps correctly.

Why does the default behavior exist at all? It's a convenience for parsing whitespace-terminated tokens, where you don't want a phantom empty string after a trailing separator. That default is defensible for interactive input; it is catastrophic for structured data formats where field position is meaningful.

The lesson generalizes: any parsing API with a "smart" default that silently trims boundary cases will bite you the moment your data isn't uniform. If you care about column count, always pass an explicit limit. Better yet, use a real CSV library (OpenCSV, Jackson CSV, Apache Commons CSV) that handles quoting, escaping, and empty fields correctly — because the trailing-empty trap is only the tip of what a hand-rolled split(",") gets wrong.

Key Takeaway: String.split(regex) silently drops trailing empty strings; pass split(regex, -1) whenever field position matters, or reach for a real CSV parser.

All newsletters