Datum en tijd in Java
Sinds het begin van de ontwikkelingen voor JDK 7 is er al redelijk wat discussie gaande over de API’s in de standaard Java libraries die betrekking hebben tot het werken met datums en tijd. In de huidige Java versie (1.6) zijn er grofweg drie (groepen) met classes die hiervoor gebruikt kunnen worden: Date en Calendar, de classes voor het opmaken van een datum voor weergave en de classes in de java.sql package. Onder deze laatste groep valt java.sql.Date, java.sql.Time en java.sql.Timestamp. De meeste softwareontwikkelaars zijn het er wel over eens dat deze API’s verre van perfect zijn.
Om dit probleem op te lossen is JSR-310 in het leven geroepen. Door gebrek aan ontwikkelaars is de voortgang van het schrijven van deze JSR en het bouwen van een referentie implementatie beperkt te noemen. Het gevolg hiervan is dat het niet zeker is of JSR-310 op tijd gereed is om onderdeel te kunnen worden van de definitieve Java 7 release (Sun heeft aangekondigd dat er een overkoepelende JSR zal komen voor JDK 7, waardoor de naam Java 7 gerechtvaardigd is. Deze naam zal ik dan ook gebruiken in de rest van dit artikel om te verwijzen naar JDK 7). JSR-310 is gestart door Stephan Colebourne, die samen met Michael Nascimento Santos de verantwoordelijke persoon is voor deze JSR. Colebourne heeft zijn sporen reeds verdiend als auteur van de redelijk populaire bibliotheek Joda Time die als vervanging kan dienen voor de standaard datum en tijd API’s in de Java SDK. Een ‘positieve’ ontwikkeling voor JSR-310 is dat de release van Java 7 is uitgesteld tot op zijn vroegst september 2010. Colebourne ziet daardoor nu reeële mogelijkheden om JSR-310, of op zijn minst een belangrijk gedeelte daarvan, toch nog onderdeel te laten zijn van Java 7.
Maar waarom is de huidige Java datum en tijd API eigenlijk niet goed genoeg? En hoe kun je de bepaalde tekortkomingen omzeilen? Hoe wil men deze problemen oplossen met JSR-310? Deze vragen zal ik proberen te beantwoorden in dit artikel.
Huidige datum en tijd API in Java
Het is een beetje teveel theorie om de volledige functionaliteit in Java 1.6 met betrekking tot datums en tijd te gaan beschrijven in dit artikel. Daarom zal ik een poging doen om de belangrijkste problemen te beschrijven en ga ik hierbij er van uit dat er wat voorkennis is van deze API. De meeste voorbeelden zijn niet echt enorm complex, dus zullen ook voor ontwikkelaars met wat minder praktijkervaring met deze API’s goed te volgen zijn.
De standaard Java 1.6 SDK bevat een relatief beperk aantal classes die zich bezig houden met datum en tijd. De belangrijkste zijn Date en Calendar. Vrijwel de volledige API is gebouwd rond deze twee classes. Het belangrijkste probleem is dat relatief eenvoudige bewerkingen op een datum of tijd eigenlijk best veel code kosten om ze uit te voeren, waarbij redelijk wat instanties van verschillende objecten moeten worden gemaakt. Behalve dat dit negatief is voor de snelheid en het geheugengebruik, komt het de leesbaarheid van de code (en dus de onderhoudbaarheid) ook niet direct ten goede. Bovendien is de bibliotheek eigenlijk ook niet compleet, waardoor vaak eigen implementaties worden gebruikt voor vaak terugkerende situaties. Tot slot heeft de huidige API ook kansen gemist om de gebruiksvriendelijkheid van de datum en tijd API te vergroten, waarbij ik met name doel op het gebrek aan een fluent API voor manipulaties en het feit dat vrijwel alle objecten die gebruikt worden om datums te representeren mutable zijn.
In de huidige API kan een specifiek moment in tijd worden gerepresenteerd met een Date object. Objecten van dit type representeren een specifiek tijdstip dat door het object intern wordt gerepresenteerd door het aantal miliseconden sinds de zogenaamde ‘Epoch’ (1 januari 1970, 0:00 uur) en een tijdszone. Zo’n gegeven zou perfect te modelleren zijn als immutable object. Helaas is daar bij het ontwikkelen van de API niet voor gekozen. In oudere Java-versies zijn er een groot aantal methods toegevoegd aan de Date class die het manipuleren van een object van dat type mogelijk maken. In de laatste Java-versies zijn de meeste van deze methods deprecated gemaakt en zouden ze dus niet meer gebruikt moeten worden. Helaas is setTime() niet deprecated, waardoor het object niet immutable is, zelfs al zou je alle deprecated methods niet gebruiken.
Het feit dat Date een mutable object is, maakt het lastig om objecten van dit type te gebruiken in andere objecten die wel immutable zijn. Voor elk veld van het type Date moet een defensieve kopie worden gemaakt om te zorgen dat het object waar deze velden onderdeel van uitmaken ook daadwerkelijk niet muteerbaar zijn. Joshua Bloch geeft een voorbeeld hiervan in zijn boek Effective Java:
public final class Period { private final Date start; private final Date end; public Period(Date start, Date end) { if (start.compareTo(end) > 0) throw new IllegalArgumentException(start + " after " + end); this.start = start; this.end = end; } public Date start() { return start; } public Date end() { return end; } }
Objecten van het type Period lijken immutable te zijn, maar door het gebruik van de Date class op een onjuiste manier, is dit niet het geval. Een voorbeeld waarin Period wordt aangepast:
Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); end.setYear(78); // Past de state van P aan!
Deze bug maakt het mogelijk om het veld end van de Period p aan te passen, waardoor de controle op de geldigheid van end en start in de constructor van het object waardeloos wordt. Uiteraard is er een relatief eenvoudige aanpassing mogelijk die dit probleem oplost:
public period(Date start, Date end) { this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); [...] // de rest is weggelaten }
Zoals is te zien, vergt dit extra aandacht van de gebruiker van de API en wordt dit vaak vergeten. Wanneer objecten van het type Date immutable zouden zijn, was deze extra kopie niet noodzakelijk geweest.
De andere class die veel gebruikt wordt wanneer met datums wordt gewerkt in Java is de Calendar class. Instanties van deze class representeren een punt op een specifiek type kalender (standaard is dit de Gregoriaanse kalender) en biedt mogelijkheden om eenvoudige berekeningen te doen met datums. Het voornaamste probleem van deze class is dat hij ook mutable is. Bovendien zijn de methods van deze class niet erg gebruiksvriendelijk. Om bijvoorbeeld het jaar van een Calendar object met de naam cal op te vragen ziet de aanroep er als volgt uit: cal.get(YEAR) waarbij YEAR een constante is die op Calendar is gedefinieerd. Mooier zou zijn als Calendar gewoon de method getYear() zou bevatten.
Het is ook verwarrend dat een object van het type Calendar in twee verschillende modi kan werken, te weten ‘lenient’ en ‘non-lenient’. In het eerste geval is het mogelijk om velden van het Calendar object waarden te geven die normaal gesproken ongeldig zouden zijn. Zo is het in dat geval mogelijk om de maand in te stellen op 15. Het Calendar object converteert dit dan automatisch naar een geldige waarde door de ‘modulus’ te nemen en het jaar met één te verhogen. Wanneer de maand januari was, wordt de nieuwe datum van de kalender dus op april ingesteld. Wacht, april? Moet dat niet maart zijn? Helaas, de Calendar class begint bij 0 te tellen wanneer het over maanden gaat (dagen, jaren, enz. zijn echter niet 0-gebaseerd).
De belangrijkste problemen zijn nu duidelijk, maar er zijn ook nog een aantal kleinere problemen. Zo bestaan ook de classes java.sql.Date, java.sql.Time en java.sql.Timestamp. Deze classes zijn ‘kinderen’ van de java.util.Date class en hebben een aangepaste functionaliteit zodat ze geschikt zijn voor gebruik in combinatie met een database. De manier waarop deze classes zijn gebouwd is echter niet zoals het zou horen. De class java.sql.Date kan worden gebruikt om een datum te representeren zonder tijd, terwijl java.sql.Time een tijd representeert zonder datum. Timestamp gebruikt een hogere resolutie voor het representeren van een tijd, aangezien deze ook nanoseconden bevat. Het probleem is dat de interne implementatie van deze classes nog steeds een complete datum en tijd bevatten en de equals en hashCode zijn niet goed geïmplementeerd, wat tot allerlei obscure problemen kan leiden:
Date date = new Date(); Time time = new Time(date.getTime()); java.sql.Date sqldate = new java.sql.Date(date.getTime()); Timestamp timestamp = new Timestamp(date.getTime()); System.out.println(date.toString()); // Sun Dec 20 15:53:55 CET 2009 System.out.println(time.toString()); // 15:53:55 System.out.println(sqldate.toString()); // 2009-12-20 System.out.println(timestamp.equals(date)); // false System.out.println(date.equals(timestamp)); // true
Het grootste probleem is dat java.sql.Timestamp niet aan de juiste voorwaarden voldoet voor de implementatie van de equals method, aangezien een van de eisen is dat deze method ‘symmetrisch’ is. Bovendien is het verwarrend dat java.sql.Date eigenlijk het tijd-component van de datum negeert, maar dit wel meeneemt in de equals method.

Astronomical Clock
En er zijn nog meer problemen met de huidige API. Het is bijvoorbeeld niet mogelijk om te werken met tijdsduur, intervallen of om op een betrouwbare manier een datum zonder tijd of een tijd zonder datum te representeren. Verder is de performance van de API onvoorspelbaar, aangezien de velden van de objecten worden herberekend op tamelijk onverwachte momenten. Tot slot is het vrij omslachtig om een datum volgens een speciale opmaak af te drukken op het scherm. Hiervoor moet een (Simple)DateFormat-object worden gebruikt, wat weer extra object instantiaties tot gevolg heeft.
Joda Time
Er is op dit moment al een werkende oplossing voor het probleem wat in de vorige sectie is beschreven. Het betreft een vervangende datum en tijd library die bekend is onder de naam Joda Time. Vrijwel alle genoemde problemen die bestaan in de huidige datum en tijd API in de Java SDK zijn afwezig in Joda Time.
Een datum of tijd kan in Joda Time op twee verschillende manieren worden gerepresenteerd: een instant wat een bepaald punt in het ‘datum tijd continuum’ betreft en een partial wat geschikt is om een gedeelte van een datum of tijd te representeren. In dit laatste geval moet worden gedacht aan een datum zonder tijd, of een datum zonder jaartal of tijdzone.
Instants worden in Joda Time geïmplementeerd door classes die de ReadableInstant-interface implementeren. De meest gebruikte class die deze interface implementeert is DateTime. Een DateTime object is immutable en beschikt over een fluent API om berekeningen te doen en kopieën te maken. Om te zorgen dat Joda Time makkelijk in reeds bestaande projecten te integreren is, beschikt DateTime over methods om van en naar een java.util.Date-object te converteren. Tot slot is er ook nog een mutable versie van het DateTime die onder de voorspellende naam MutableDateTime door het leven gaat. Deze class kan worden gebruikt wanneer er veel aanpassingen of berekeningen aan een datum dienen plaats te vinden. Normaal gesproken wordt een object van dit type dan naar een immutable DateTime-object geconverteerd voor verder gebruik. MutableDateTime is in die zin dus te vergelijken met een StringBuilder voor Strings in Java.
Partials worden in Joda Time gerepresenteerd door objecten die de ReadablePartial-interface implementeren. De meest gebruikte classes die deze interface implementeert zijn LocalTime, LocalDate en LocalDateTime. Ook deze objecten zijn immutable en kunnen makkelijk worden geconverteerd naar DateTime of JDK Date objecten. Verder beschikt Joda Time over een Duration-, Period- en Interval-class die ook het werken met deze vormen van tijd mogelijk maken.
Uiteraard kan een voorbeeld van het gebruik van Joda Time niet achterwege blijven:
DateTime now = new DateTime(); Period sixdays = new Period(0, 0, 0, 6, 6, 0, 0, 0); System.out.println(now); // 2009-12-20T18:35:53.360+01:00 DateTime future = now.plus(sixdays); System.out.println(future); // 2009-12-27T00:41:59.097+01:00 System.out.println(now.withYear(2028).plusDays(31).toString("dd-MM-yyyy hh:mm")); // 20-01-2029 06:41
Dit voorbeeld initialiseert de variable now met de huidige tijd (de standaard waarde van een DateTime object dat wordt gecreëerd zonder parameters). De variabele sixdays bevat een referentie naar een Period-object die een periode van zes dagen en zes uur representeert. Period-objecten kunnen eenvoudig worden opgeteld bij een DateTime-object met behulp van de plus()-method. Wel dient er op gelet te worden dat het resultaat van deze method-aanroep aan een (nieuwe) variabele wordt toegekend, aangezien niet het bestaande object wordt aangepast, maar een nieuw object wordt gemaakt met de nieuwe waarde.
De laatste regel van het voorbeeld toont hoe eenvoudig het is eenvoudige berekeningen met datums te doen in Joda Time. Bovendien wordt ook nog getoond he makkelijk het is om een datum te printen volgens een bepaalde formattering. Om zoiets te doen met de standaard Java datum-functionaliteit, zouden er redelijk wat nieuwe objecten moeten worden aangemaakt, namelijk een Calendar, Date en SimpleDateFormat.
Ook het converteren van bestaande datums naar DateTime of andere objecten binnen Joda Time is erg eenvoudig. De meeste classes die gebruikt worden om een datum te bevatten beschikken namelijk over een constructor die veel verschillende datatypen accepteert. Zo is het bijvoorbeeld mogelijk om een string met een datum volgens het ISO 8601-formaat in de constructor van DateTime mee te geven. Dit ISO 8601-formaat wordt bijvoorbeeld ook gebruikt door de XML-standaard. Tot slot zorgt Joda Time ervoor dat de code beter testbaar is, aangezien de mogelijkheid bestaat om de huidige tijd aan te passen met behulp van de aanroep DateTimeUtils.setCurrentMillisFixed(millis);.
JSR-310 en Joda Time
Zoals eerder gezegd wordt JSR-310 geleid door Stephan Colebourne die ook verantwoordelijk is voor Joda Time. Het zou logisch kunnen zijn om de laatste Joda Time-release te pakken, deze te voorzien van een officiële specificatie en dit het JSR-310 stickertje te geven. Dit is echter niet wat er gebeurt, en met goede reden. Colebourne heeft in een recente blog post beschreven waarom hij niet voor die aanpak heeft gekozen. De belangrijkste reden is dat Joda Time ook niet perfect is.
De ontwerpdoelen voor JSR-310 zijn duidelijk gedefinieerd: de API moet immutable ondersteunen een een fluent API bieden, de API moet eenvoudig uitbreidbaar zijn, het gebruik van de API moet duidelijk, eenduidig en voorspelbaar zijn. Vooral op dit laatste punt vertoond Joda Time gebreken. Een voorbeeld hiervan is het gebruik van het Chronology-object om verschillende kalendertypen te ondersteunen. Het is mogelijk om een Chronology-object mee te geven wanneer een DateTime-object wordt gemaakt. Normaal gesproken kan een DateTime object een waarde tussen 1 en 12 teruggeven wanneer de method getMonthOfDay() wordt aangeroepen. Wanneer er echter een Coptic Chronology wordt meegegeven bij het instantiëren van een DateTime-object, kan de getMonthOfDay()-method ook de waarde 13 teruggeven. Dit zal de gebruiker eigenlijk nooit verwachten, en om de API dus betrouwbaar te kunnen gebruiken zou altijd moeten worden gecheckt wat de onderliggende Chronology van het object is voordat de getMonthOfDay()-method wordt aangeroepen. Daarnaast vind Colbeourne ook het feit dat DateTime de ReadableInstant en daardoor dus Instant ongelukkig, omdat er eigenlijk verschil zou moeten worden gemaakt tussen hoe mensen ‘tijd’ definiëren en hoe computers dit doen. Tot slot is het accepteren van null-waarden door veel methods ook een ontwerpfout, aldus Colebourne.
De problemen die nu nog in Joda Time aanwezig zijn, zouden in JSR-310 opgelost moeten worden zodat het uiteindelijk nog een betere datum- en tijd-API wordt. Desalniettemin is Joda Time op dit moment de beste keuze wanneer met datums wordt gewerkt in een Java-applicatie. Het feit dat de ontwikkeling van JSR-310 niet echt snel verloopt is dan ook deels toe te schrijven aan het gegeven dat Joda Time voor de meeste toepassingen zeer geschikt is, waardoor de noodzaak voor een betere versie eigenlijk grotendeels afwezig is.
Conclusie
De datum en tijd-functies in de standaard Java SDK vertonen op een aantal punten serieuze gebreken. Hoewel het voor de meeste projecten mogelijk is om deze wel te gebruiken, biedt het alternatief duidelijke voordelen tegen weinig kosten. JSR-310 kan in de toekomst het probleem definitief oplossen, maar er bestaat nog steeds de kans dat het geen onderdeel uit zal maken van Java 7 – voornamelijk vanwege gebrek aan ontwikkelaars.
Voor nieuwe projecten zijn er weinig goede argumenten om niet Joda Time te gebruiken in plaats van de standaard Java-API. Er is goede ondersteuning voor Joda Time en uitgebreid getest in de praktijk en ook Hibernate en JSP’s worden ondersteund. Wanneer Joda Time wordt gebruikt is het eenvoudiger om berekeningen met datums te doen dankzij de eenvoudige API en betrouwbare performance.

[...] Dutch version can be found at the Finalist IT Group weblog. [...]
Martin @ Blog » Blog Archive » Java Date and Time API and JSR-310 January 7, 2010 10:47
Goed artikel, jammer dat de release van Java 7 nog zo ver weg is, laat staan het /gebruik/ ervan.
Auke van Leeuwen January 11, 2010 18:56
“Objecten van dit type representeren een specifiek tijdstip dat door het object intern wordt gerepresenteerd door het aantal miliseconden sinds de zogenaamde ‘Epoch’ (1 januari 1970, 0:00 uur) en een tijdszone.”
Dat is niet helemaal waar – java.util.Date objecten bevatten alleen een aantal milliseconden (sinds 01-01-1970 00:00:00 UTC), geen timezone. Eén van de dingen die vaak verwarrend is voor programmeurs is dat class Date niets weet van timezones.
“Om bijvoorbeeld het jaar van een Calendar object met de naam cal op te vragen ziet de aanroep er als volgt uit: cal.get(YEAR) waarbij YEAR een constante is die op Calendar is gedefinieerd.”
Dit is inderdaad erg lelijk, een vorm van het magical strings anti-pattern: http://wiki.apidesign.org/wiki/MagicalStrings
Jesper de Jong April 28, 2010 16:12