ZonedDateTime — τι είναι, εργασία με ζώνες ώρας και χρόνο

Συγγραφέας: IT Sectr Δημοσιεύτηκε: 2026-07-13 Χρόνος ανάγνωσης: 10 λεπ

ZonedDateTime — μια αμετάβλητη κλάση από το πακέτο java.time, που αποθηκεύει ημερομηνία και ώρα μαζί με πληροφορίες ζώνης ώρας (ZoneId). Σε αντίθεση με το LocalDateTime, το ZonedDateTime προσδιορίζει μοναδικά μια χρονική στιγμή. Σύμφωνα με τις προδιαγραφές Oracle Java 17 (2024), η κλάση χειρίζεται σωστά τη μετάβαση στη θερινή ώρα (DST) μέσω κανόνων ζώνης από τη βάση δεδομένων IANA Time Zone Database.

Κύρια σημεία

  • ZonedDateTime — μια αμετάβλητη κλάση που συνδυάζει ημερομηνία, ώρα και ζώνη ώρας (ZoneId) σε ένα αντικείμενο.
  • Σε αντίθεση με το LocalDateTime, το ZonedDateTime προσδιορίζει μοναδικά μια χρονική στιγμή και είναι κατάλληλο για παγκόσμια συστήματα.
  • Η κλάση χειρίζεται αυτόματα τη μετάβαση στη θερινή ώρα (DST) σύμφωνα με τους κανόνες της IANA Time Zone Database.
  • Για μετατροπή μεταξύ ζωνών ώρας χρησιμοποιείται η μέθοδος withZoneSameInstant(ZoneId).
  • Η αποθήκευση ZonedDateTime σε βάσεις δεδομένων συνιστάται μέσω OffsetDateTime ή TIMESTAMP WITH TIME ZONE.

Τι είναι το ZonedDateTime;

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 vs LocalDateTime: ποια η διαφορά;

Η κύρια διαφορά — το 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;

Η ζώνη ώρας στο 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

Το 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).

kotlin
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).

kotlin
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"))

Εργασία με τη θερινή ώρα (DST)

Η μετάβαση στη θερινή ώρα δημιουργεί δύο προβλήματα: "κενά" (gap) και "επικαλύψεις" (overlap). Το κενό εμφανίζεται την άνοιξη, όταν τα ρολόγια μετακινούνται μπροστά — συγκεκριμένη ώρα δεν υπάρχει. Η επικάλυψη — το φθινόπωρο, όταν η ώρα γυρίζει πίσω — η ίδια ώρα υπάρχει δύο φορές.

Το ZonedDateTime χειρίζεται αυτές τις καταστάσεις μέσω της στρατηγικής resolve. Κατά τη δημιουργία αντικειμένου κατά τη διάρκεια κενού, το java.time μετατοπίζει αυτόματα την ώρα κατά το μέγεθος της μετατόπισης. Κατά τη δημιουργία κατά τη διάρκεια επικάλυψης, επιλέγεται η πρώτη επιλογή (πριν από τη μεταφορά). Η συμπεριφορά μπορεί να αλλάξει μέσω withZoneSameInstant.

Μπορείτε να ελέγξετε αν η ώρα βρίσκεται σε ζώνη DST μέσω zone.getRules().isDaylightSavings(instant). Η μέθοδος getOffset() δείχνει την τρέχουσα μετατόπιση για αυτήν τη χρονική στιγμή, και getRules().getDaylightSavings(instant) — το μέγεθος της προσαρμογής DST σε χιλιοστά του δευτερολέπτου.

kotlin
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

Για μορφοποίηση του 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().

kotlin
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 στο Android: πρακτικά παραδείγματα

Πρώτο παράδειγμα — εμφάνιση ώρας συνάντησης για τον χρήστη στη ζώνη ώρας του. Ο διακομιστής στέλνει ZonedDateTime σε UTC, ο πελάτης μετατρέπει στην τοπική ζώνη ώρας της συσκευής.

kotlin
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() για τον υπολογισμό της διαφοράς.

kotlin
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.

kotlin
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 και OffsetDateTime;

Το ZonedDateTime περιέχει αναγνωριστικό περιφερειακής ζώνης (π.χ. "Europe/Moscow") και χειρίζεται DST. OffsetDateTime αποθηκεύει μόνο σταθερή μετατόπιση (+03:00) χωρίς περιφερειακούς κανόνες. Για αποθήκευση σε βάση δεδομένων συνιστάται το OffsetDateTime.

Πώς να λάβετε την τρέχουσα ώρα σε UTC μέσω ZonedDateTime;

Χρησιμοποιήστε ZonedDateTime.now(ZoneOffset.UTC) ή Instant.now().atZone(ZoneOffset.UTC). Και οι δύο επιλογές επιστρέφουν την τρέχουσα χρονική στιγμή με μηδενική μετατόπιση. Για απλή χρονική σήμανση χρησιμοποιήστε Instant.now() χωρίς σύνδεση με ζώνη.

Μπορεί το ZonedDateTime να σειριοποιηθεί μέσω Gson ή Moshi;

Ναι, αλλά απαιτείται προσαρμοσμένος προσαρμογέας. Το Gson δεν υποστηρίζει ZonedDateTime από προεπιλογή. Το Moshi — υποστηρίζει μέσω του προσαρμογέα Rfc3339DateJsonAdapter. Συνιστάται η χρήση Kotlinx Serialization ή της βιβλιοθήκης JavaTimeModule για Jackson.

Πώς να χειριστείτε την περίπτωση όταν η ώρα πέφτει σε "κενό" DST;

Το java.time μετατοπίζει αυτόματα την ώρα μπροστά κατά το μέγεθος της μετατόπισης. Για παράδειγμα, αν η ώρα 02:30 δεν υπάρχει κατά τη μετάβαση σε 03:00, το ZonedDateTime θα δημιουργήσει αντικείμενο στις 03:30. Μπορείτε να ελέγξετε την ύπαρξη κενού μέσω ZoneRules.getTransition(instant).

Γιατί το ZonedDateTime δεν συνιστάται για βάσεις δεδομένων SQL;

Το JDBC 4.2 υποστηρίζει OffsetDateTime, αλλά όχι απευθείας ZonedDateTime. Το ZonedDateTime περιέχει περιφερειακή ζώνη, η οποία δεν έχει αντίστοιχο στην SQL. Συνιστάται να αποθηκεύετε OffsetDateTime ή Instant, και να αποθηκεύετε τη ζώνη σε ξεχωριστή στήλη.

Σύνοψη

  • ZonedDateTime — αμετάβλητη κλάση για ημερομηνία και ώρα με ζώνη ώρας, που χειρίζεται σωστά το DST μέσω IANA Time Zone Database.
  • Η κύρια διαφορά από το LocalDateTime — η παρουσία ζώνης, που καθιστά το ZonedDateTime μοναδικό αναγνωριστικό χρονικής στιγμής.
  • Για μετατροπή μεταξύ ζωνών χρησιμοποιήστε withZoneSameInstant(), που διατηρεί τη χρονική στιγμή, όχι το withZoneSameLocal.
  • Κατά τη μετάβαση στη θερινή ώρα, το java.time επιλύει αυτόματα "κενά" και "επικαλύψεις" μέσω ενσωματωμένων κανόνων ζώνης.
  • Για αποθήκευση σε βάση δεδομένων χρησιμοποιήστε OffsetDateTime ή αποθηκεύστε Instant και ZoneId ξεχωριστά.
  • Στο Android για μετατροπή ZonedDateTime σε τοπική ώρα συσκευής χρησιμοποιήστε ZoneId.systemDefault() σε συνδυασμό με withZoneSameInstant.
  • Κατά τη σειριοποίηση σε JSON απαιτείται προσαρμοσμένος προσαρμογέας — χρησιμοποιήστε Kotlinx Serialization ή Jackson JavaTimeModule.

Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση

Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.

Συζήτηση έργου

Διαβάστε επίσης