ZonedDateTime — μια αμετάβλητη κλάση από το πακέτο java.time, που αποθηκεύει ημερομηνία και ώρα μαζί με πληροφορίες ζώνης ώρας (ZoneId). Σε αντίθεση με το LocalDateTime, το ZonedDateTime προσδιορίζει μοναδικά μια χρονική στιγμή. Σύμφωνα με τις προδιαγραφές Oracle Java 17 (2024), η κλάση χειρίζεται σωστά τη μετάβαση στη θερινή ώρα (DST) μέσω κανόνων ζώνης από τη βάση δεδομένων IANA Time Zone Database.
Κύρια σημεία
ZonedDateTime — μία από τις βασικές κλάσεις του πακέτου java.time, που αντιπροσωπεύει ημερομηνία και ώρα με πλήρεις πληροφορίες ζώνης ώρας. Συνδυάζει τρία στοιχεία: LocalDateTime (ημερομηνία και ώρα), ZoneId (αναγνωριστικό ζώνης) και ZoneOffset (μετατόπιση σε σχέση με το UTC).
Σε αντίθεση με το LocalDateTime, το οποίο αποθηκεύει μόνο την τοπική ώρα (wall-clock time) χωρίς σύνδεση με ζώνη, το ZonedDateTime προσδιορίζει μοναδικά μια χρονική στιγμή. Δύο ίδια LocalDateTime σε διαφορετικές ζώνες ώρας αντιπροσωπεύουν διαφορετικές χρονικές στιγμές. Δύο ίδια ZonedDateTime — την ίδια χρονική στιγμή.
Η κλάση είναι πλήρως αμετάβλητη (immutable) και thread-safe. Όλες οι αριθμητικές πράξεις επιστρέφουν νέο αντικείμενο. Το ZonedDateTime υλοποιεί τη διεπαφή ChronoZonedDateTime και μπορεί να χρησιμοποιηθεί οπουδήποτε απαιτείται εργασία με ζωνική ώρα σε Java.
Σύμφωνα με προδιαγραφές Oracle Java 17, το ZonedDateTime υποστηρίζει εργασία με οποιαδήποτε ζώνη από την IANA Time Zone Database, η οποία περιλαμβάνει πάνω από 600 ζώνες ώρας.
Η κύρια διαφορά — το ZonedDateTime περιέχει ζώνη ώρας, ενώ το LocalDateTime όχι. Αυτή η θεμελιώδης διαφορά καθορίζει το πεδίο εφαρμογής κάθε κλάσης.
Το LocalDateTime χρησιμοποιείται για τοπικά συμβάντα: ώρα συναυλίας, πρόγραμμα μαθημάτων, ημερομηνία γέννησης. Αν ένα συμβάν γίνεται στη Μόσχα στις 15:00, το LocalDateTime θα καταγράψει 15:00 χωρίς σύνδεση. Αν μεταφέρετε τον διακομιστή στη Νέα Υόρκη, η ώρα θα παραμείνει 15:00 — αλλά αυτή θα είναι διαφορετική φυσική ώρα.
Το ZonedDateTime χρησιμοποιείται για παγκόσμια δεδομένα: αρχεία καταγραφής διακομιστή, χρονικές σημάνσεις σε API, διεθνείς συναντήσεις. Αν μια συνάντηση έχει προγραμματιστεί για τις 15:00 MSK, το ZonedDateTime θα αποθηκεύσει και την ώρα και τη ζώνη. Στη Νέα Υόρκη θα εμφανίζεται σωστά ως 8:00 EST. Σύμφωνα με Baeldung (2024), η επιλογή μεταξύ LocalDateTime και ZonedDateTime είναι η συχνότερη αρχιτεκτονική απόφαση κατά την εργασία με ημερομηνίες.
Πρακτικός κανόνας: αν τα δεδομένα αποθηκεύονται για μία περιοχή — χρησιμοποιήστε LocalDateTime. Αν τα δεδομένα διασχίζουν σύνορα ζωνών ώρας — χρησιμοποιήστε ZonedDateTime. Αν χρειάζεται να μεταδώσετε μια απόλυτη χρονική στιγμή — χρησιμοποιήστε Instant.
Η ζώνη ώρας στο java.time αντιπροσωπεύεται από την κλάση ZoneId. Το ZoneId είναι ένα αναγνωριστικό ζώνης σε μορφή "continent/region", για παράδειγμα "Europe/Moscow", "America/New_York", "Asia/Tokyo". Το ZoneId αποκτάται μέσω της στατικής μεθόδου of(String zoneId) ή μέσω της προεπιλεγμένης ζώνης ώρας συστήματος.
Το ZoneId χωρίζεται σε δύο τύπους: fixed offset (σταθερή μετατόπιση, π.χ. "+03:00") και region-based (περιφερειακές ζώνες, π.χ. "Europe/London"). Οι περιφερειακές ζώνες περιέχουν κανόνες μετάβασης στη θερινή ώρα και ιστορικές αλλαγές. Το Fixed offset είναι απλά μια σταθερή μετατόπιση.
Για να λάβετε την τρέχουσα μετατόπιση του ZoneId σε μια συγκεκριμένη χρονική στιγμή, χρησιμοποιείται η μέθοδος getRules(), η οποία επιστρέφει ZoneRules. Το ZoneRules περιέχει όλες τις μεταβάσεις και μετατοπίσεις για αυτήν τη ζώνη. Αυτός είναι ο βασικός μηχανισμός για σωστή επεξεργασία DST.
Όλες οι ζώνες ώρας παρέχονται με το JDK μέσω αρχείων tzdata (IANA Time Zone Database) και ενημερώνονται τακτικά. Στο Android, η έκδοση tzdata εξαρτάται από ενημερώσεις συστήματος μέσω του Google Play Services.
Το ZonedDateTime μπορεί να δημιουργηθεί με διάφορους τρόπους. Ο πιο απλός — now(), που επιστρέφει την τρέχουσα ώρα στην προεπιλεγμένη ζώνη ώρας συστήματος. Η παραλλαγή now(ZoneId) επιτρέπει τη λήψη της τρέχουσας ώρας σε μια συγκεκριμένη ζώνη.
Η μέθοδος of(LocalDateTime, ZoneId) δημιουργεί ZonedDateTime από τοπική ώρα και ζώνη. Η παραλλαγή of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone) — από στοιχεία.
Το LocalDateTime μπορεί να μετατραπεί σε ZonedDateTime μέσω της μεθόδου atZone(ZoneId). Instant — μέσω Instant.atZone(ZoneId). Date — μέσω Date.toInstant().atZone(ZoneId).
val moscowZone = ZoneId.of("Europe/Moscow")
val nowInMoscow = ZonedDateTime.now(moscowZone)
val fromComponents = ZonedDateTime.of(
2026, 7, 21, 15, 30, 0, 0, moscowZone
)
val fromLocal = LocalDateTime.now().atZone(moscowZone)
val fromInstant = Instant.now().atZone(moscowZone)
Η κύρια μέθοδος μετατροπής — withZoneSameInstant(ZoneId). Μετατρέπει το ZonedDateTime σε άλλη ζώνη ώρας, διατηρώντας την ίδια χρονική στιγμή. Για παράδειγμα, 15:00 MSK → 8:00 EST. Η μέθοδος withZoneSameLocal(ZoneId) αλλάζει τη ζώνη διατηρώντας την τοπική ώρα — αυτό δίνει διαφορετική χρονική στιγμή.
Για να λάβετε τη μετατόπιση σε σχέση με το UTC χρησιμοποιείται η μέθοδος getOffset(), που επιστρέφει ZoneOffset. Το ZoneOffset είναι κληρονόμος του ZoneId, που αντιπροσωπεύει σταθερή μετατόπιση σε μορφή "+HH:mm" ή "-HH:mm".
Η μετατροπή σε Instant γίνεται μέσω της μεθόδου toInstant(). Το Instant είναι μια απόλυτη χρονική στιγμή, ανεξάρτητη από ζώνη ώρας. Η αντίστροφη μετατροπή — Instant.atZone(ZoneId).
val moscow = ZonedDateTime.of(
2026, 7, 21, 15, 0, 0, 0,
ZoneId.of("Europe/Moscow")
)
val newYork = moscow.withZoneSameInstant(
ZoneId.of("America/New_York")
)
val utcInstant = moscow.toInstant()
val backToMoscow = utcInstant.atZone(ZoneId.of("Europe/Moscow"))
Η μετάβαση στη θερινή ώρα δημιουργεί δύο προβλήματα: "κενά" (gap) και "επικαλύψεις" (overlap). Το κενό εμφανίζεται την άνοιξη, όταν τα ρολόγια μετακινούνται μπροστά — συγκεκριμένη ώρα δεν υπάρχει. Η επικάλυψη — το φθινόπωρο, όταν η ώρα γυρίζει πίσω — η ίδια ώρα υπάρχει δύο φορές.
Το ZonedDateTime χειρίζεται αυτές τις καταστάσεις μέσω της στρατηγικής resolve. Κατά τη δημιουργία αντικειμένου κατά τη διάρκεια κενού, το java.time μετατοπίζει αυτόματα την ώρα κατά το μέγεθος της μετατόπισης. Κατά τη δημιουργία κατά τη διάρκεια επικάλυψης, επιλέγεται η πρώτη επιλογή (πριν από τη μεταφορά). Η συμπεριφορά μπορεί να αλλάξει μέσω withZoneSameInstant.
Μπορείτε να ελέγξετε αν η ώρα βρίσκεται σε ζώνη DST μέσω zone.getRules().isDaylightSavings(instant). Η μέθοδος getOffset() δείχνει την τρέχουσα μετατόπιση για αυτήν τη χρονική στιγμή, και getRules().getDaylightSavings(instant) — το μέγεθος της προσαρμογής DST σε χιλιοστά του δευτερολέπτου.
fun checkDST(zdt: ZonedDateTime) {
val rules = zdt.getZone().getRules()
val instant = zdt.toInstant()
if (rules.isDaylightSavings(instant)) {
val dstAmount = rules.getDaylightSavings(instant)
Log.d("Θερινή Ώρα", "Μετατόπιση θερινής ώρας: $dstAmount")
}
}
Για μορφοποίηση του ZonedDateTime χρησιμοποιείται DateTimeFormatter. Η τυπική μορφή ISO περιλαμβάνει ημερομηνία, ώρα και μετατόπιση: "2026-07-21T15:30:00+03:00[Europe/Moscow]". Προκαθορισμένες μορφές: ISO_ZONED_DATE_TIME, ISO_OFFSET_DATE_TIME, ISO_INSTANT.
Για τοπική μορφοποίηση χρησιμοποιήστε DateTimeFormatter.ofLocalizedDateTime(FormatStyle). Το FormatStyle μπορεί να είναι SHORT, MEDIUM, LONG, FULL. Το LONG περιλαμβάνει το όνομα ζώνης ("MSK"), το FULL — πλήρες όνομα ("Moscow Standard Time").
Σημαντικό: κατά την ανάλυση συμβολοσειράς με ZonedDateTime, η μορφή πρέπει να περιέχει πληροφορίες ζώνης ή μετατόπισης. Αν η ζώνη δεν αναφέρεται, χρησιμοποιήστε LocalDateTime.parse() και στη συνέχεια atZone().
val zdt = ZonedDateTime.now(ZoneId.of("Europe/Moscow"))
val iso = zdt.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)
val custom = DateTimeFormatter
.ofPattern("dd.MM.yyyy HH:mm z")
val formatted = zdt.format(custom)
val parsed = ZonedDateTime.parse(
"2026-07-21T15:30:00+03:00",
DateTimeFormatter.ISO_OFFSET_DATE_TIME
)
Πρώτο παράδειγμα — εμφάνιση ώρας συνάντησης για τον χρήστη στη ζώνη ώρας του. Ο διακομιστής στέλνει ZonedDateTime σε UTC, ο πελάτης μετατρέπει στην τοπική ζώνη ώρας της συσκευής.
fun displayMeetingTime(
serverUtc: ZonedDateTime
): String {
val deviceZone = ZoneId.systemDefault()
val localTime = serverUtc.withZoneSameInstant(deviceZone)
val formatter = DateTimeFormatter
.ofPattern("dd.MM.yyyy HH:mm z")
return localTime.format(formatter)
}
Δεύτερο παράδειγμα — υπολογισμός χρόνου μέχρι το επόμενο γεγονός με υπολογισμό ζώνης ώρας. Χρησιμοποιούμε ZonedDateTime για τον χρόνο διακομιστή και Duration.between() για τον υπολογισμό της διαφοράς.
fun timeUntilEvent(eventTime: ZonedDateTime): String {
val now = ZonedDateTime.now()
val duration = Duration.between(now, eventTime)
val hours = duration.toHours()
val minutes = duration.toMinutes() % 60
return "Remaining $hours h $minutes min"
}
Τρίτο παράδειγμα — εργασία με API Retrofit. Ο διακομιστής επιστρέφει συμβολοσειρά σε ISO-8601 με ζώνη. Χρησιμοποιούμε προσαρμοσμένο αποσειριοποιητή για μετατροπή σε ZonedDateTime.
data class EventResponse(
@JsonAdapter(ZonedDateTimeAdapter::class)
val eventTime: ZonedDateTime
)
class ZonedDateTimeAdapter : JsonAdapter<ZonedDateTime>() {
override fun fromJson(reader: JsonReader): ZonedDateTime? {
return ZonedDateTime.parse(
reader.nextString()
)
}
}
Πρώτο σφάλμα — χρήση ZoneId.systemDefault() σε κώδικα διακομιστή. Η ζώνη ώρας του διακομιστή μπορεί να διαφέρει από αυτήν του πελάτη, και η χρήση της ζώνης συστήματος στον διακομιστή οδηγεί σε λανθασμένους υπολογισμούς. Πάντα να αναφέρετε ρητά τη ζώνη ή να χρησιμοποιείτε UTC ως πρότυπο.
Δεύτερο σφάλμα — αγνόηση DST κατά τον υπολογισμό διάρκειας. Το Duration.between() χειρίζεται σωστά τις μεταβάσεις, αλλά αν αφαιρείτε timestamp χειροκίνητα, η μετάβαση στη θερινή ώρα μπορεί να δώσει σφάλμα 1 ώρας. Χρησιμοποιήστε μεθόδους ChronoUnit.HOURS.between() αντί για χειροκίνητα μαθηματικά.
Τρίτο σφάλμα — σύγχυση μεταξύ withZoneSameInstant και withZoneSameLocal. Το πρώτο αλλάζει ζώνη διατηρώντας τη χρονική στιγμή — η ώρα μετατοπίζεται. Το δεύτερο αλλάζει ζώνη διατηρώντας την τοπική ώρα — η χρονική στιγμή αλλάζει. Η επιλογή λάθος μεθόδου είναι ένα από τα συχνότερα σφάλματα σύμφωνα με SonarSource (2024).
Τέταρτο σφάλμα — η υπόθεση ότι η ζώνη ώρας της συσκευής είναι πάντα ίδια με τη ζώνη ώρας του χρήστη. Ο χρήστης μπορεί να ταξιδεύει και να περιμένει ότι η εφαρμογή θα δείξει την ώρα στη "οικεία" του ζώνη, όχι στην τρέχουσα. Σε αυτήν την περίπτωση, πρέπει να παρέχετε επιλογή ζώνης μέσω διεπαφής.
Συχνές ερωτήσεις
Το ZonedDateTime περιέχει αναγνωριστικό περιφερειακής ζώνης (π.χ. "Europe/Moscow") και χειρίζεται DST. OffsetDateTime αποθηκεύει μόνο σταθερή μετατόπιση (+03:00) χωρίς περιφερειακούς κανόνες. Για αποθήκευση σε βάση δεδομένων συνιστάται το OffsetDateTime.
Χρησιμοποιήστε ZonedDateTime.now(ZoneOffset.UTC) ή Instant.now().atZone(ZoneOffset.UTC). Και οι δύο επιλογές επιστρέφουν την τρέχουσα χρονική στιγμή με μηδενική μετατόπιση. Για απλή χρονική σήμανση χρησιμοποιήστε Instant.now() χωρίς σύνδεση με ζώνη.
Ναι, αλλά απαιτείται προσαρμοσμένος προσαρμογέας. Το Gson δεν υποστηρίζει ZonedDateTime από προεπιλογή. Το Moshi — υποστηρίζει μέσω του προσαρμογέα Rfc3339DateJsonAdapter. Συνιστάται η χρήση Kotlinx Serialization ή της βιβλιοθήκης JavaTimeModule για Jackson.
Το java.time μετατοπίζει αυτόματα την ώρα μπροστά κατά το μέγεθος της μετατόπισης. Για παράδειγμα, αν η ώρα 02:30 δεν υπάρχει κατά τη μετάβαση σε 03:00, το ZonedDateTime θα δημιουργήσει αντικείμενο στις 03:30. Μπορείτε να ελέγξετε την ύπαρξη κενού μέσω ZoneRules.getTransition(instant).
Το JDBC 4.2 υποστηρίζει OffsetDateTime, αλλά όχι απευθείας ZonedDateTime. Το ZonedDateTime περιέχει περιφερειακή ζώνη, η οποία δεν έχει αντίστοιχο στην SQL. Συνιστάται να αποθηκεύετε OffsetDateTime ή Instant, και να αποθηκεύετε τη ζώνη σε ξεχωριστή στήλη.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης