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 ที่เหมือนกัน — ช่วงเวลาเดียวกัน
คลาสนี้ไม่สามารถเปลี่ยนแปลงได้อย่างสมบูรณ์และปลอดภัยต่อเยราดดำเนินการ (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 เป็นตัวระบุโซนในรูปแบบ "ทวีป/ภูมิภาค" เช่น "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"))
การเปลี่ยนแปลงเวลาออมแสงสร้างสองปัญหา: ช่องว่าง (gaps) และ การซ้อนทับ (overlaps) ช่องว่างเกิดขึ้นในฤดูใบไม้ผลิเมื่อเปลี่ยนแปลงเวลาไปข้างหน้า — เวลาบางอย่างไม่มีอยู่ การซ้อนทับเกิดขึ้นในฤดูใบไม้ร่วงเมื่อเปลี่ยนแปลงเวลากลับ — เวลาเดียวกันเกิดขึ้นสองครั้ง
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"
}
ตัวอย่างที่สาม — การทำงานกับ Retrofit API เซิร์ฟเวอร์คืนสตริง 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() จัดการเปลี่ยนแปลงได้อย่างถูกต้อง, แต่หากคุณลบแสดงเวลาด้วยตนเอง, เวลาออมแสงอาจทำให้เกิดความผิดพลาด 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 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ