Leer y escribir CSV en Java

Java no trae un lector de CSV, y el formato tiene más trampas de las que parece: el separador cambia según el país, los campos pueden llevar comillas dentro y los decimales dependen del idioma. Con cuarenta líneas se resuelve bien.

Escribir

try (var w = Files.newBufferedWriter(f, StandardCharsets.UTF_8)) {
    w.write("nombre;ciudad;precio\n");
    for (var x : filas)
        w.write(String.join(";", cita(x.nombre()), cita(x.ciudad()),
                            String.format(Locale.US, "%.2f", x.precio())) + "\n");
}

// entrecomilla si hay separador, comillas o saltos de línea
String cita(String v) {
    if (v.contains(";") || v.contains("\"") || v.contains("\n"))
        return '"' + v.replace("\"", "\"\"") + '"';
    return v;
}
datos.csvnombre;ciudad;precio
gorra;Madrid;12.50
"bufanda ""larga""";Bilbao;9.00
"mochila; grande";A Coruña;34.00

Dos detalles que separan un CSV que funciona de uno que rompe la hoja de cálculo del cliente:

  • El entrecomillado. Si el campo contiene el separador, un salto de línea o comillas, va entre comillas, y las comillas de dentro se duplican. Ahí está bufanda ""larga"".
  • Locale.US en el format. Sin eso, en una máquina configurada en español el precio saldría 12,50 con coma… que es justo el separador de muchos CSV. Es un fallo que no aparece en tu portátil y sí en el servidor.
Y el juego de caracteres explícito: StandardCharsets.UTF_8. Desde Java 18 el valor por omisión ya es UTF-8, pero escribirlo documenta la intención y protege de una configuración rara. Si el destino es Excel en Windows, puede que necesites windows-1252 o un BOM.

Leer

try (var r = Files.newBufferedReader(f, StandardCharsets.UTF_8)) {
    String cab = r.readLine();
    String linea;
    while ((linea = r.readLine()) != null) {
        String[] c = partir(linea, ';');
        System.out.printf("  %-18s %-10s %8.2f%n", c[0], c[1], Double.parseDouble(c[2]));
    }
}
salidacabecera: [nombre, ciudad, precio]
  gorra              Madrid        12.50
  bufanda "larga"    Bilbao         9.00
  mochila; grande    A Coruña      34.00

Volvieron enteros, con las comillas deshechas y el punto y coma de «mochila; grande» intacto. Eso lo hace el analizador, que es la pieza que de verdad importa:

String[] partir(String linea, char sep) {
    var out = new ArrayList<String>();
    var actual = new StringBuilder();
    boolean dentro = false;
    for (int i = 0; i < linea.length(); i++) {
        char c = linea.charAt(i);
        if (dentro) {
            if (c == '"' && i + 1 < linea.length() && linea.charAt(i + 1) == '"') { actual.append('"'); i++; }
            else if (c == '"') dentro = false;
            else actual.append(c);
        } else if (c == '"') dentro = true;
        else if (c == sep) { out.add(actual.toString()); actual.setLength(0); }
        else actual.append(c);
    }
    out.add(actual.toString());
    return out.toArray(new String[0]);
}
Lo que nunca hay que hacer es linea.split(";"). Funciona hasta el primer campo que lleve un punto y coma dentro, y entonces te desplaza todas las columnas en silencio. Si el archivo viene de fuera, siempre hay uno.

Lo que esto no cubre

Este analizador vale para CSV bien formado de una línea por registro. Lo que le falta: campos con saltos de línea dentro de las comillas, que obligan a leer por caracteres y no por líneas, y la detección automática del separador. Si el archivo es ajeno y grande, lo sensato es Commons CSV o FastCSV, que además son bastante más rápidos.

Todo el código se compiló y se ejecutó con Java 25 LTS en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.