Java Datum-Parsing
Strings in Java-Datumsobjekte umwandeln mit DateTimeFormatter und Parsing-Ausnahmen korrekt behandeln.
Parsing ist das Spiegelbild von Formatierung. Gleicher DateTimeFormatter, gleiche Muster-Alphabetik, gleiche Besonderheiten — aber ein String wird gelesen statt geschrieben. Jeder java.time-Typ hat eine parse(...)-Factory-Methode; mit einem Standardmuster (ISO-8601) nimmt sie ein Argument, mit einem benutzerdefinierten Muster nimmt sie einen Formatter.
LocalDate d = LocalDate.parse("2025-11-04"); // ISO default
LocalTime t = LocalTime.parse("14:30:00");
LocalDateTime dt = LocalDateTime.parse("2025-11-04T14:30:00"); // note the T
ZonedDateTime zdt = ZonedDateTime.parse("2025-11-04T14:30:00-05:00[America/New_York]");
Instant i = Instant.parse("2025-11-04T19:30:00Z"); // trailing Z mandatoryJeder dieser Aufrufe verwendet den Standard-Formatter des jeweiligen Typs — striktes ISO-8601. Für alles, was keine ISO-Form hat, erstellt man einen Formatter und übergibt ihn.
Parsing mit benutzerdefiniertem Muster
DateTimeFormatter f = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate d = LocalDate.parse("04/11/2025", f); // British DMYDie Muster-Alphabetik ist dieselbe wie bei der Formatierung — dd MMM yyyy, HH:mm:ss, MM/dd/yyyy h:mm a. Die Übereinstimmungsregel ist standardmäßig strikt: Jedes Literalzeichen im Muster muss im Input exakt vorhanden sein, und jedes Feld muss die richtige Stellenanzahl haben.
DateTimeFormatter dmy = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate.parse("4/11/2025", dmy); // FAILS: "dd" requires 2 digits
LocalDate.parse("04/11/2025", dmy); // OKWenn der Input manchmal einstellig ist, verwende d/M/yyyy (das 1-2 Stellen akzeptiert) oder erstelle den Formatter mit DateTimeFormatterBuilder und parseStrict(false). Die einbuchstabige Form ist die einfachere Lösung.
Locale spielt beim Parsing eine Rolle
Dieselbe Locale-Problematik wie bei der Formatierung: Monatsnamen (MMMM) und Wochentagsnamen (EEEE) sind sprachspezifisch, daher muss der Formatter wissen, in welcher Sprache der Input verfasst ist.
DateTimeFormatter englishDay = DateTimeFormatter.ofPattern("EEEE, MMMM d yyyy", Locale.US);
DateTimeFormatter germanDay = DateTimeFormatter.ofPattern("EEEE, d. MMMM yyyy", Locale.GERMAN);
LocalDate.parse("Tuesday, November 4 2025", englishDay); // 2025-11-04
LocalDate.parse("Dienstag, 4. November 2025", germanDay); // 2025-11-04 (German month name)Beachte yyyy in beiden Mustern. Um ein LocalDate zu erzeugen, muss der Input eine Jahreszahl liefern — ein Muster wie "EEEE, MMMM d" parst zwar, aber nur zu einem TemporalAccessor ohne Jahresfeld, sodass LocalDate.parse darauf wirft. Wenn die Strings tatsächlich kein Jahr enthalten, parse zu einem TemporalAccessor und kombiniere das Ergebnis selbst mit einer Jahreszahl.
Ohne explizite Locale wird Locale.getDefault() verwendet — und die Standard-Locale einer Server-JVM ist unvorhersehbar. Übergib immer eine Locale, wenn Monats- oder Tagesnamen geparst werden, die ein Benutzer eintippen könnte. Das Spiegelprinzip „immer mit Locale formatieren" gilt auch hier.
DateTimeParseException
Ein fehlgeschlagenes Parsing wirft eine DateTimeParseException (eine Unterklasse von RuntimeException, also nicht auf parse deklariert). Die Meldung nennt sowohl die Position als auch das Erwartete:
try {
LocalDate.parse("2025-13-45"); // month 13, day 45
} catch (DateTimeParseException e) {
e.getParsedString(); // "2025-13-45"
e.getErrorIndex(); // index where parsing gave up
e.getMessage(); // human description
}Zwei verschiedene Fehlerarten landen hier:
- Format-Nichtübereinstimmung. Der String passt überhaupt nicht zum Muster —
"04 nov 2025"gegen"dd-MM-yyyy". - Wert außerhalb des gültigen Bereichs. Der String passt zum Muster, aber ein Wert ist unmöglich — Monat 13, Tag 32.
Beide werfen dieselbe Klasse. Abfangen und melden; niemals stillschweigend schlucken.
Zweistellige Jahresangaben und die MAX_VALUE-Falle
Das yy-Muster (zweistellige Jahreszahl) hat ein dokumentiertes, aber überraschendes Standardverhalten: Es parst zu dem Jahr, das am nächsten zu heute liegt, innerhalb eines 100-Jahres-Fensters. LocalDate.parse("11/04/25", DateTimeFormatter.ofPattern("MM/dd/yy")) ist 2025-11-04 im Jahr 2025 und 2125-11-04 im Jahr 2076. Das ist eine nützliche Eigenschaft für „nahe bei heute"-Fälle und eine Fehlerquelle für Archivdaten.
Die Lösung ist, yyyy zu verwenden, wenn der Input vier Stellen hat, und das Jahrhundert-Fenster explizit anzugeben, wenn er das nicht tut:
DateTimeFormatter f = new DateTimeFormatterBuilder()
.appendPattern("MM/dd/")
.appendValueReduced(ChronoField.YEAR, 2, 2, 1950) // window starts at 1950
.toFormatter();Wenn du Legacy-Daten mit yy verarbeitest, dokumentiere das Fenster im Code. Der Standard ist „rollierender Pivot um das aktuelle Jahr", was nicht das Richtige ist für „alle meine Daten stammen aus den 1980ern".
Parsing ohne Festlegung auf einen Typ
DateTimeFormatter.parse(String) gibt einen TemporalAccessor zurück — die unterste Ebene der Typhierarchie. Nützlich, wenn der Input entweder ein LocalDate oder ein LocalDateTime sein könnte:
TemporalAccessor ta = DateTimeFormatter.ISO_DATE_TIME.parseBest(
"2025-11-04T14:30:00",
LocalDateTime::from, // preferred
LocalDate::from); // fallbackparseBest(text, ...queries) versucht die from-Methode jedes Typs der Reihe nach und gibt den ersten erfolgreichen zurück. Das Ergebnis benötigt instanceof, um es für etwas Konkretes zu verwenden:
if (ta instanceof LocalDateTime ldt) ...
else if (ta instanceof LocalDate ld) ...Für die meisten Fälle ist der direkte Aufruf von parse(...) auf dem spezifischen Typ einfacher. parseBest ist für den Fall gedacht, dass mehrere Formen akzeptiert werden (eine CSV-Spalte, die entweder ein Datum oder ein Datum-Uhrzeit-Wert sein könnte).
Tolerantes Parsing mit dem Builder
DateTimeFormatterBuilder ermöglicht das Zusammensetzen eines Formatters, der toleranter ist:
DateTimeFormatter lenient = new DateTimeFormatterBuilder()
.appendPattern("yyyy-MM-dd[ HH:mm[:ss]]") // optional sections in []
.parseLenient() // accept missing leading zeros etc.
.parseCaseInsensitive() // ignore case on month/day names
.toFormatter(Locale.US);Die Klammersyntax [...] markiert einen optionalen Abschnitt — dieses Muster parst sowohl "2025-11-04" (ohne Uhrzeit) als auch "2025-11-04 14:30" (mit Uhrzeit). Kombiniert mit parseLenient und parseCaseInsensitive lässt sich ein Formatter erstellen, der eine größere Bandbreite von Eingaben akzeptiert, ohne einen eigenen Parser zu schreiben.
Das ist übertrieben für Code, der beide Enden kontrolliert. Verwende den strikten Standard, es sei denn, du liest Benutzereingaben oder Legacy-Daten.
Instant.parse ist strikt
Instant.parse("2025-11-04T19:30:00Z") funktioniert. Das abschließende Z (UTC) ist Pflicht; jeder andere Offset (-05:00, +09:00) erfordert zunächst OffsetDateTime.parse oder ZonedDateTime.parse, dann .toInstant():
Instant inst = OffsetDateTime.parse("2025-11-04T14:30:00-05:00").toInstant();Das ist die kanonische Konvertierung, wenn eine externe API ISO-8601-Strings mit Zeitzonen-Offsets, aber ohne IANA-Zone liefert.
Ein ausgearbeitetes Beispiel: Konfiguration lesen, parsen, auf fehlerhafte Eingabe reagieren
Das folgende Programm parst drei datumsartige Strings aus einer synthetischen Konfiguration: ein ISO-Datum, ein benutzerdefiniertes Musterdatum und einen ISO-Instant. Es demonstriert dann den toleranten Builder mit einem optionalen Zeitabschnitt, die parseBest-API für „entweder ein Datum oder ein Datum-Uhrzeit-Wert" und den Fehlerfall, wenn der Input nicht passt.
Was aus der Ausführung zu entnehmen ist:
- Die drei ISO-Standard-Parser akzeptierten jeweils genau die Standardform:
yyyy-MM-ddfürLocalDate, dieT-getrennte Form fürLocalDateTimeund dieZ-terminierte Form fürInstant. Keine Flexibilität, kein Raten — und das ist der Sinn. Wenn der Input passt, wird kein Formatter benötigt; wenn nicht, ist der Aufbau eines Formatters eine Zeile. - Der tolerante Formatter akzeptierte drei verschiedene Input-Formen — nur Datum, Datum mit Uhrzeit, Datum mit Uhrzeit und Sekunden — weil der eingeklammerte
[...]-Abschnitt optional ist.parseBest(text, LocalDateTime::from, LocalDate::from)wählte den reichhaltigsten Typ, den jeder Input unterstützte. Das ist das richtige Muster, wenn Benutzer-eingegebene oder Konfigurations-Daten mit variabler Präzision akzeptiert werden. OffsetDateTime.parse(wire).toInstant()war die kanonische Brücke von „einem ISO-8601-Timestamp mit einem Offset" zuInstant.Instant.parseselbst akzeptiert nurZ-suffixiertes UTC; alles andere muss zuerst überOffsetDateTime(oderZonedDateTime) gehen. Die Konvertierung ist in jede Richtung eine Zeile.- Das deutsche Locale-Parsing funktionierte nur, weil der Formatter mit
Locale.GERMANgebaut wurde. Die Standard-Locale hätte"November"abgelehnt, wenn die JVM auf Deutsch läuft (was erwartet, dassLocale.GERMANs"November"gegen die deutschen Namen abgeglichen wird). Locale immer am Parse-Rand festsetzen — der Formatter allein reicht nicht; die Locale steuert die Auflösung von Monats- und Tagesbezeichnungen. - Die beiden Fehlerblöcke warfen beide
DateTimeParseExceptionmit nützlicher Positionsinformation.getErrorIndexist der Ort, an dem der Parser aufgegeben hat;getParsedStringist der Input, wie der Parser ihn gesehen hat. Diese Informationen in benutzer-sichtbaren Fehlern ausgeben — „Datum konnte nicht bei Zeichen 5 geparst werden" ist wesentlich hilfreicher als „ungültiges Datum".
Wie geht es weiter
Formatierung und Parsing schließen die String-Grenze ab. Das nächste Kapitel, Java Temporal Adjusters, kehrt zur Wertseite zurück und behandelt die eingebauten Adjusters (firstDayOfMonth, nextOrSame(MONDAY) usw.) sowie das Schreiben eigener Adjusters — nützlich immer dann, wenn das gewünschte Datum vom vorhandenen Datum abhängt („erster Dienstag nach dem 15.").