Blogg
Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn
Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn
It was the best of
java.time.Date
, it was the worst ofjava.utl.Date
.
- not Charles Dickens
Använder du forfarande java.util.Date
?
Gör inte det.
Varför? Det finns bättre sätt att hantera både kalenderdatum och tid i java.time
paketet. Ta 10 minuter för att förstå vad klasserna i java.time
kan göra för dig.
Idag är det skottdagen 2024 så vad är lämpligare än att prata lite om datum och tid i vårt favoritspråk Java.
Vi firar snart 10-årsjubileum av att Java 8 släpptes och i den releasen hittar vi JSR-310. Den gav oss ett väldigt brett API för att arbeta med datum och tid.
Ändå ser vi än idag exempel som använder java.util.Date
och java.util.Calendar
och vi får förslag från AI-verktyg som ska hjälpa oss att koda som ser ut såhär:
Okej, vi kanske måste använda oss av java.util.*
när vi använder gamla legacy-system eller bibliotek, men nu har de ändå haft 10 år på sig att ändra på sig.
java.util.Date
var felkonstruerat redan från början när Java 1.0 släpptes 1996. Det visste man om från Sun och det existerar en del kul historier från den tiden om hur diskussionerna gick om just hanteringen av tid. Redan i 1.1 deprecerades i princip alla metoder och man flyttade dem till den abstrakta java.util.Calendar
och implementationsklassen java.util.GregorianCalendar
. Men även dessa ansågs vara mer eller mindre “trasiga”.
java.util.Date
användes brett, trots att den var gravt missförstådd av utvecklarna. Speciellt har den missförståtts och använts på ett sätt som gränsar till övergrepp av utvecklare av bibliotek och bara skapat ännu mera förvirring. Ofta var man tvingad att skapa adaptrar för att konvertera mellan olika biblioteks interna datum och tidshantering.
Det fanns bra initiativ, som Joda-time, som blev lite av en de facto standard och som sedan ledde till JSR-310. Numera rekommenderar de själva att man använder java.time
istället men släppte ändå en ny version så sent som den 4 mars 2023. Den innehåller lite små roligheter som inte finns i java.time
.
Det som varit svårast att förstå för utvecklare är att en Date
-instans representerar ett ögonblick i tiden och inte ett datum. Det gör att:
Det ger i sin tur ett antal följdproblem:
java.util.*
är muterbara!! Så om du inte ville att någon skulle kunna manipulera på ett datum som du skickade in i en annan metod, så var du tvungen att klona det först.java.util.Date
, som representerar en Datum-Tid-tupel, och hur datum representeras i SQL. Så då fick man konvertera till java.sql.Date
istället och den representerar bara ett singulärt datum. Så ofta var det i själva verket java.sql.Timestamp
som skulle ha använts. En date är inte alltid en date, så att säga.toString()
. Gissa om det har förvirrat utvecklare som sitter i olika tidzoner då java.util.Date
inte är tidzonmedveten.Något behövdes göras och i Java 8 introducerades ett helt nytt Date & Time API som addresserar de problem som fanns med java.util.Date
. Det var Stephen Colebourne (som var den som skrivit mycket av Joda-Time) och Oracle som tillsamamns skapade nya java.time
under JSR-310.
I JSR-310 drevs framför allt tre kärnidéer.
java.util.SimpleDateFormat
, var att de inte var trådsäkra.java.time
Det nya API:et lades i Java 8 till i paketet java.time
och återfinns numera i java.base
modulen. Inte helt oväntat följer den samma struktur och format som Joda-Time.
Alla klasser är oförändringsbara (immutable) och därför trådsäkra. Alla manipulationer på en klass returnerar ett nytt objekt och förmodligen finns det redan en manipulations-metod redan som du kan använda utan att behöva tänka efter eller i detalj förstå hur temporal aritmetik ska hanteras.
I java.time
paketet finns klasser för datum, tid, datum/tid, tidzoner, ögonblick (instants), varaktighet och tidsmanipulation. Alla klasser använder sig av ISO-8601 formatet.
Några av de vanligast använda klassern är:
System.currentTimeMillis()
.Exempel:
// Få fram det lokala datumet
final LocalDate date = LocalDate.now();
// Få fram den lokala tiden
final LocalTime time = LocalTime.now();
// Få fram den lokala date/time
final LocalDateTime datetime = LocalDateTime.now();
// eller
final LocalDateTime dateTime = LocalDateTime.of(date, time);
// Räkna ut hur länge det är mellan två tidpunkter
final LocalDateTime from = LocalDateTime.of(2024, Month.January, 18, 17, 0, 0, 0);
final LocalDateTime to = LocalDateTime.of(2025, Month.January, 24, 13, 0, 0, 0);
// Tid/Dagar mellan Cadec 2024 och 2025.
final Duration duration = Duration.between(from, to);
final Period period = Period.between(from, to);
Den här koden demonstrerar skillnaden mellan just Duration (som tidsaxel) och Period (kalenderfokuserad).
final LocalDateTime to = LocalDateTime.of(2025, Month.JANUARY, 24, 13, 0, 0, 0);
// Varaktighet
final Duration duration = Duration.between(from, to);
// Skriv ut varaktigheten
System.out.println("Duration: does not handle years, " +
" does not handle months, " +
duration.toDays() + " days," +
duration.toHours() + " hours."); // 24 * days + 20 timmar.
// Konvertera LocalDateTime till LocalDate och skapa en Period
final Period period = Period.between(from.toLocalDate(), to.toLocalDate());
// Skriv ut perioden.
System.out.println("Period: " + period.getYears() + " years, " +
period.getMonths() + " months, " +
period.getDays() + " days.");
En av de metoder som kom till i Java 9 var möjligheten att skapa en Stream av datum med hjälp av datesUntil(LocalDate endExclusive). Man kan också skapa en stream med Period som argument och då enkelt kunna filtrera fram t.ex. måndagar.
LocalDate startDate = LocalDate.of(2024, 1, 1);
LocalDate endDate = LocalDate.of(2024, 12, 31);
// Filtrera ut Måndagar med hjälp av datesUntil() och filter()
var mondays = startDate.datesUntil(endDate.plusDays(1))
.filter(date -> date.getDayOfWeek() == DayOfWeek.MONDAY)
.collect(Collectors.toList());
// Skriv ut alla måndagar under 2024.
for (LocalDate monday : mondays) {
System.out.println(monday);
}
Ytterst användbart när man vill titta på statistik eller göra något speciellt om det är en specifik typ av dag.
Parsning av strängar till objekt i java.time
görs med parse(String str)
metoden. De finns alltid även i en variant där man kan skicka med en DateTimeFormatter
.
En sak som är värd att nämna är parsning av varaktigheter och perioder. ISO-8601 definierar ett format för att beskriva varaktigheter som P[n]Y[n]M[n]DT[n]H[n]M[n]S
eller P[n]W
. Eftersom man i Java har två klasser där Duration hanterar tid upp till dagar, t.ex P2DT3H30M
och Period hanterar dagar eller mer (allt fram till T) så blir parsningen av ISO-strängar för varaktigheter lite knepigare. Man behöver dela upp parsningen så att Period hanterar det som är till vänster om T
och Duration det till höger. Har kan det vara bra att titta på Joda-Time.
String isoDurationString = "P2Y6M14DT12H12M12S";
String[] durationParts = isoDurationString.split("T");
Period period = Period.parse(durationParts[0]);
Duration duration = Duration.parse("PT" + durationParts[1]);
System.out.println(period.toTotalMonths());
System.out.println(duration.toMinutes());
Om man inte skapar sitt eget format, används ISO-formaten som t.ex ISO_ZONED_DATE_TIME som använder formatet 2011-12-03T10:15:30+01:00[Europe/Paris]
. I DateTimeFormatter finns de flesta av de vanligaste ISO-formateringarna som konstanter.
På samma sätt kan man använda sig av metoden format(DateTimeFormatter formatter)
för att formatera en temporal till det format man behöver. Alla toString()
-metoder använder sig av ISO-8601-formatet.
DateTimeFormatter är, till skillnad från sin föregångare i java.util
, trådsäker och går att återanvända mellan olika trådar.
Detaljer om formatterare och andra klasser att titta på finns i paketet java.time.format
.
I sitt grundutförande så används ISO-8601 kalender, eller det som kallades GregorianCalendar java.util
. Andra kalenderformat, så som Japanska kalendern, hittar man i java.time.chrono
. Java 9 lade till en antal kalendra och förfinade dem.
En java.util.Date
representerar i paraktiken ett ögonblick på tidslinjen, i UTC, och som en kombination av tid på dagen och vilket datum. Det gör att vi kan konverterar det till ett antal olika typer i java.time
beroende på vårt faktiska användningsfall.
I Java 8 lade man till metoden toInstant()
som gör att man kan konvertera en java.util.Date
och java.util.Calendar
till java.time.Instant
. java.sql.Date
kommer däremot att kasta ett exception om man anropar toInstant()
. Det är för att java.sql.Date
inte har någon tids-komponent. Det finns en metod toLocalDate()
som kan användas istället. java.sql.Timestamp
har däremot både toInstant()
och toLocalDateTime()
. Det blir lätt förvirrande.
Ett exempel på hur man kan använda `toInstant():
java.util.Date date = ...;
java.util.Calendar calendar = ...;
LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
LocalDateTime.ofInstant(calendar.toInstant(), ZoneId.systemDefault());
När Java 8 kom var det få externa bibliotek, så som XML, JSON och JPA som hade stöd för java.time
. Nu, 10 år senare, har lyckligtvis alla de stora anpassat sig den nya tiden.
Om du trots det hittar bilbiotekt som forfarande använder java.util.Date
så har du två alternativ, i prioritetsordning:
Java har som princip att alltid försöka vara bakåtkompatibelt så långt det går. Där för är alla metoder i java.util
som har med datum och tid att göra inte deprekerade, men många kodanalysverktyg och IDE:er varnar idag om man använder sig av dessa klasser.
Sedan JSR-310 har det egentligen inte hänt några större saker i förhållande till java.time
. Det har mest varit anpassningar av andra delar av Javas standardbibliotek. Några noterbara saker som hänt är:
java.time
de facto standard i alla delar av Java som använder datum eller tid. Java 11 var den första LTS-versionen efter Java 8 och mycket resurser lades på att inget i Javas standardbibliotek längre skulle använda java.util
.atStartOfDay(ZoneId)
över tidzoner.Om du inte redan har gjort det, ta en halv dag och gör dig familjär med klasserna i java.time
. Det är helt klart värt java.time.Duration.ofHours(4)
.