Daten in Dateien
Kurz gesagt: CSV ist eine Textdatei, in der jede Zeile ein Datensatz ist und die Werte durch Kommas getrennt sind; das Programm teilt die Zeilen in Teile und macht daraus Objekte.
Problem
Text, aber du brauchst Zahlen
Das Lager exportiert seine Produkte in eine Datei. Jede Zeile ist ein Produkt: Name, Preis und Bestand, durch Kommas getrennt. Die erste Zeile sagt, was in jeder Spalte steht:
Name,Preis,BestandBrot,1.20,30Milch,2.10,18Kaffeebohnen,18.90,12Die Datei lesen kannst du schon. Aber für das Programm ist die Zeile nur Text:
String line = "Brot,1.20,30";System.out.println(line.length());System.out.println(line.charAt(5));121Es weiß nicht, dass 1.20 ein Preis ist und 30 eine Anzahl. Um auszurechnen, was die Ware wert ist, musst du jede Zeile in etwas mit Feldern verwandeln: in ein Product-Objekt.
Die Idee
CSV: Zeile zu Objekt
So eine Datei heißt CSV (comma-separated values, „durch Kommas getrennte Werte“). Excel liest sie, Datenbanken lesen sie, jedes Programm liest sie. Eine Zeile wird in drei Schritten zum Objekt: split, jeden Teil in den passenden Typ umwandeln, der Konstruktor.
class Product { String name; double price; int stock;
Product(String name, double price, int stock) { this.name = name; this.price = price; this.stock = stock; }
static Product fromCsv(String line) { String[] f = line.split(","); if (f.length != 3) { throw new IllegalArgumentException("3 Spalten erwartet, es sind " + f.length); } return new Product(f[0].strip(), Double.parseDouble(f[1].strip()), Integer.parseInt(f[2].strip())); }
String toCsv() { return name + "," + price + "," + stock; }}Product p = Product.fromCsv("Kaffeebohnen,18.90,12");System.out.printf("%s: %.2f € x %d = %.2f €%n", p.name, p.price, p.stock, p.price * p.stock);Kaffeebohnen: 18.90 € x 12 = 226.80 €fromCsv ist die einzige Stelle, die weiß, wie die Zeile aussieht. Ändert sich das Format, änderst du nur sie. Die Prüfung der Spaltenanzahl wirft eine IllegalArgumentException mit einer klaren Meldung, statt dass das Programm später mit einem Index außerhalb des Arrays abbricht.
Was fromCsv mit der Zeile macht, Schritt für Schritt, mit seinem Rumpf in main ausgeschrieben:
String line = "Kaffeebohnen,18.90,12";String[] f = line.split(",");String name = f[0];double price = Double.parseDouble(f[1]);int stock = Integer.parseInt(f[2]);Product p = new Product(name, price, stock);System.out.println(p.name + ": " + p.price + " € x " + p.stock);System.out.printf("%.2f €%n", p.price * p.stock);Schritt für Schritt, in Worten
- Die Zeile aus der Datei ist ein String. Für das Programm ist die 18.90 darin nur vier Zeichen.
- split schneidet die Zeile an jedem Komma: ein Array aus drei Strings.
- Der Name bleibt ein String.
- parseDouble macht aus dem String "18.90" die Zahl 18.9. Mit ihr kannst du jetzt rechnen.
- parseInt macht aus "12" die ganze Zahl 12.
- Der Konstruktor legt die drei Werte in die Felder eines neuen Product, und p zeigt darauf.
- p.price wird als 18.9 ausgegeben: Die Null am Ende stand nur im Text.
- Der Preis mal den Bestand: Jetzt ist es eine Rechnung mit Zahlen.
Sieh es dir an
Die ganze Datei
Path file = Path.of("products.csv");try { Files.writeString(file, "Name,Preis,Bestand\nBrot,1.20,30\nMilch,2.10,18\nKaffeebohnen,18.90,12\n"); List<String> lines = Files.readAllLines(file); Product[] products = new Product[lines.size() - 1]; int count = 0; boolean header = true; for (String line : lines) { if (header) { header = false; continue; } products[count] = Product.fromCsv(line); count++; } double value = 0; for (int i = 0; i < count; i++) { value += products[i].price * products[i].stock; } System.out.printf("%d Produkte, Warenwert %.2f €%n", count, value);} catch (IOException e) { System.out.println("Fehler beim Lesen von " + file + ": " + e.getMessage());}3 Produkte, Warenwert 300.60 €- Das Array hat ein Element weniger als die Zeilen, weil die erste Zeile die Kopfzeile ist.
headerist nur im ersten Durchlauftrue. Dann wird die Zeile mitcontinueübersprungen.countzählt, wie viele Produkte bisher im Array sind, und ist der Index für das nächste.
Im Modul „Collections“ siehst du eine Liste, die von selbst wächst, und dann brauchst du das Zählen mit count nicht mehr.
Die Idee
Fehlerhafte Zeilen
Die Dateien schreiben Menschen und andere Programme. Früher oder später kommt eine Zeile ohne eine Spalte, mit einem Wort statt einer Zahl, oder eine leere Zeile:
Name,Preis,BestandBrot,1.20,30Milch,2.10Kaffee,sieben,4
Tee,3.50,25Ein Fehler soll nicht das Laden von allem anderen stoppen. Jede Zeile steht in ihrem eigenen try, und die Meldung nennt die Nummer der Zeile, damit jemand sie korrigieren kann:
Path file = Path.of("products.csv");try { Files.writeString(file, "Name,Preis,Bestand\nBrot,1.20,30\nMilch,2.10\nKaffee,sieben,4\n\nTee,3.50,25\n"); List<String> lines = Files.readAllLines(file); Product[] products = new Product[lines.size()]; int count = 0; int row = 0; for (String line : lines) { row++; if (row == 1 || line.isBlank()) { continue; } try { products[count] = Product.fromCsv(line); count++; } catch (NumberFormatException e) { System.out.println("Zeile " + row + ": ungültige Zahl (" + e.getMessage() + ")"); } catch (IllegalArgumentException e) { System.out.println("Zeile " + row + ": " + e.getMessage()); } } System.out.println("Geladen: " + count);} catch (IOException e) { System.out.println("Fehler beim Lesen von " + file + ": " + e.getMessage());}Zeile 3: 3 Spalten erwartet, es sind 2Zeile 4: ungültige Zahl (For input string: "sieben")Geladen: 2- Leere Zeilen werden mit
isBlank()übersprungen. NumberFormatExceptionist eine Unterklasse vonIllegalArgumentException, deshalb steht sie zuerst. Sonst würde das allgemeinecatchauch sie fangen.- Das Programm sagt „Geladen: 2“ und macht mit ihnen weiter.
Probier es aus
Leere Spalten
Was gibt das Programm aus?
System.out.println("Kaffee,,4".split(",").length);System.out.println("Kaffee,7.50,".split(",").length);System.out.println("Kaffee,7.50,".split(",", -1).length);,,) bleibt als "". Die leeren Teile am Ende entfernt split, deshalb gibt "Kaffee,7.50," nur 2 Teile, und f[2] würde eine Exception werfen. Mit dem zweiten Argument -1 bleiben die leeren Teile am Ende. Für fromCsv heißt das: Eine Zeile mit einer leeren letzten Spalte zählt als Zeile mit 2 Spalten.Die Idee
Objekte zu CSV
Die andere Richtung ist eine Methode, die die Zeile zusammenbaut:
String toCsv() { return name + "," + price + "," + stock;}Du schreibst die Kopfzeile und danach eine Zeile für jedes Objekt:
Product[] stock = { new Product("Brot", 1.2, 30), new Product("Milch", 2.1, 18) };Path file = Path.of("stock.csv");try { try (BufferedWriter out = Files.newBufferedWriter(file)) { out.write("Name,Preis,Bestand"); out.newLine(); for (Product p : stock) { out.write(p.toCsv()); out.newLine(); } } System.out.print(Files.readString(file));} catch (IOException e) { System.out.println("Fehler beim Schreiben von " + file + ": " + e.getMessage());}Name,Preis,BestandBrot,1.2,30Milch,2.1,18fromCsv kann diese Datei wieder einlesen. Speichern und Lesen müssen dasselbe Format verwenden.
Vorsicht, wenn du die Preise mit String.format speicherst. Es folgt den Spracheinstellungen des Computers, und im Deutschen und in vielen anderen Sprachen ist das Dezimalzeichen ein Komma:
String de = String.format(Locale.of("de"), "%s,%.2f,%d", "Brot", 1.2, 30);System.out.println(de + " -> " + de.split(",").length + " Spalten");String root = String.format(Locale.ROOT, "%s,%.2f,%d", "Brot", 1.2, 30);System.out.println(root + " -> " + root.split(",").length + " Spalten");Brot,1,20,30 -> 4 SpaltenBrot,1.20,30 -> 3 Spalten1,20 wird zu zwei Spalten, und die Zeilen verschieben sich. Für Dateien, die ein anderes Programm liest, gib Locale.ROOT an (java.util.Locale). Damit kommt immer ein Punkt heraus, auf jedem Computer.
Probier es aus
Finde den Fehler
Das Programm soll die drei Produkte aus products.csv laden, aber es bricht schon bei der ersten Zeile ab:
Exception in thread "main" java.lang.NumberFormatException: For input string: "Preis"Wähle die Zeile mit dem Fehler.
row wird schon im ersten Durchlauf zu 1, vor der Prüfung, deshalb ist row == 0 nie wahr. Die Kopfzeile wird nicht übersprungen, und fromCsv versucht, das Wort „Preis“ in eine Zahl umzuwandeln. Die Korrektur ist row == 1.
Die Idee
Spalten nach Namen
Ein anderes Programm exportiert dieselben Daten, aber mit den Spalten in einer anderen Reihenfolge und mit einer Spalte mehr. Schreibst du f[1] für den Preis, liest du bei einem neuen Format die falsche Spalte und merkst es nicht. Sicherer ist es, die Spalte über ihren Namen in der Kopfzeile zu finden:
static int column(String[] header, String name) { for (int i = 0; i < header.length; i++) { if (header[i].strip().equalsIgnoreCase(name)) { return i; } } throw new IllegalArgumentException("Keine Spalte " + name);}
Path file = Path.of("export.csv");try { Files.writeString(file, "Bestand,Name,Nummer,Preis\n30,Brot,1001,1.20\n18,Milch,1002,2.10\n"); int nameCol = -1; int priceCol = -1; for (String line : Files.readAllLines(file)) { String[] f = line.split(","); if (nameCol == -1) { nameCol = column(f, "name"); priceCol = column(f, "preis"); continue; } System.out.println(f[nameCol] + ": " + f[priceCol] + " €"); }} catch (IOException e) { System.out.println(e.getMessage());}Brot: 1.20 €Milch: 2.10 €Der erste Durchlauf findet nur, wo die Spalten sind. Danach wird jede Zeile über diese Indizes gelesen. Gibt es die Spalte nicht, wirft column sofort eine Exception, statt falsche Daten zu lesen.
Genauer, für Neugierige
- Ein Komma im Wert. „Gouda, mittelalt“ enthält ein Komma. In CSV kommt so ein Wert in Anführungszeichen:
"Gouda, mittelalt",9.80,5, und ein Anführungszeichen darin wird doppelt geschrieben:"". Dann reichtsplit(",")nicht mehr. In echten Projekten nimmt man eine Bibliothek wie OpenCSV oder Apache Commons CSV, die alle Regeln kennt. - Ein anderes Trennzeichen. Excel auf einem Computer mit deutschen Einstellungen speichert CSV oft mit
;, genau weil das Komma das Dezimalzeichen ist. - Serialisierung. Java kann mit
ObjectOutputStreamein ganzes Objekt in eine Datei schreiben, wenn die KlasseSerializableimplementiert. Die Datei ist dann binär: Ein Mensch kann sie nicht lesen, und sie geht kaputt, wenn sich die Klasse ändert. Textformate wie CSV und JSON sind berechenbarer und werden deshalb öfter verwendet.
Übung
Daten, die lebendig werden
Löse die Aufgaben in deinem Editor. Das Programm legt die Eingabedatei zuerst selbst an.
1. CSV-Auswertung. Lege people.csv mit dem Inhalt unten an. Gib das Durchschnittsalter aus (Ganzzahldivision), wer am ältesten ist und wie viele Personen aus jeder Stadt kommen, in der Reihenfolge, in der die Städte vorkommen. Die Städte speicherst du in einem Array String[], die Anzahlen in einem int[] derselben Größe.
Name,Alter,StadtJonas,34,BerlinMaria,28,HamburgThomas,41,BerlinAnna,25,MünchenDurchschnittsalter: 32Am ältesten: Thomas, 41Personen aus Berlin: 2Personen aus Hamburg: 1Personen aus München: 12. Den Helden speichern. Eine Klasse Hero mit Name, Level, Leben und Gold. Die Methode save(Path file) speichert den Helden als Zeilen schlüssel=wert, und die statische Methode load(Path file) erzeugt aus so einer Datei einen neuen Hero. Erzeuge „Arin“ mit Level 6, 100 Leben und 120 Gold, erhöhe sein Level um 1, nimm ihm 15 Leben, gib ihm 220 Gold, speichere ihn, gib die Datei aus und lade ihn wieder.
name=Arinlevel=7health=85gold=340Geladen: Arin, Level 7, Leben 85, Gold 3403. Doppelter Text. Lege text.txt mit dem Text unten an. Teile ihn am Punkt (und den Leerzeichen danach) in Sätze und gib jeden Satz, der mehr als einmal vorkommt, einmal aus.
Das ist Text. Das ist auch Text. Das ist Text.Doppelter Text: "Das ist Text."Weitere Aufgaben
- Das teuerste Produkt. Lade
products.csvaus der Lektion und gib das Produkt mit dem höchsten Preis aus und wie viele Produkte einen Bestand unter 20 haben. - Korrigierte Zeilen. Lade die Datei mit den fehlerhaften Zeilen und schreibe die guten nach
products-clean.csvund die fehlerhaften, mit der Nummer der Zeile und dem Grund, nachrejected.txt. - Umsatzbericht. Eine Datei
sales.csvmit den SpaltenDatum,Produkt,Anzahl,Preis. Die Zeilen sind nach Datum sortiert. Gib den Umsatz für jeden Tag mit String.format in Spalten aus.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Werfen und eigene Exceptions · Strings · Reguläre Ausdrücke · Dateien
- Geht weiter in: Datenbanken
- Aufgaben zum Thema: Text und Dateien