ZonedDateTime — یک کلاس immutable از بسته 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 را اجرا میکند و میتواند در هر جایی که کار با زمان منطقهای در جاوا مورد نیاز است استفاده شود.
بر اساس مشخصات 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 را میتوان از طریق روش atZone(ZoneId) به ZonedDateTime تبدیل کرد. 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("DST", "جابجایی DST: $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() انتقالات را به درستی پردازش میکند، اما اگر تایمستمپها را دستی کم کنید، انتقال به ساعت تابستانی میتواند خطای ۱ ساعته ایجاد کند. به جای محاسبه دستی، از روشهای 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 پشتیبانی میکند. برای Jackson استفاده از Kotlinx Serialization یا کتابخانه JavaTimeModule توصیه میشود.
java.time به طور خودکار زمان را به اندازه جابجایی جلو میبرد. مثلاً، اگر زمان 02:30 در انتقال به 03:00 وجود نداشته باشد، ZonedDateTime شیئِ 03:30 را ایجاد میکند. میتوان وجود شکاف را از طریق ZoneRules.getTransition(instant) بررسی کرد.
JDBC 4.2 از OffsetDateTime پشتیبانی میکند، اما نه مستقیماً ZonedDateTime. ZonedDateTime شامل منطقه منطقهای است که در SQL هیچ معادلی ندارد. توصیه میشود OffsetDateTime یا Instant را ذخیره کرده و منطقه را در یک ستون جداگانه ذخیره کنید.
نتیجهگیری
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید