Java 8 Date & Time API

วันนี้จะมาเล่าเรื่องของคลาสชุด "วันและเวลา" บน Java ให้ฟังนิดหน่อยครับ

วันเก่า ๆ ...

ตั้งแต่ Java 1.0 เรามีคลาสที่ดูแลเรื่องของ "วันและเวลา" อยู่หนึ่งคลาส ชื่อว่า ... Date ซึ่ง จากมุมมองของนักพัฒนาหลาย ๆ คน คลาสนี้เป็นคลาสที่เรียกได้ว่าออกแบบมาได้แย่ที่สุดของ Java เลยทีเดียว ด้วยเหตุผลที่ว่า มันผูกวันที่และเวลาเอาไว้ในคลาสเดียว

หลังจากนั้นไม่นาน ใน Java 1.1 ทาง Sun ก็มีการเพิ่มคลาสที่เกี่ยวข้องกับเวลาเข้ามาอีกหนึ่งคลาส มีชื่อว่า Calendar ด้วยเหตุผลด้านการจัดการ Timezone การรองรับ Internationalization และตัว year parameter ใน method ต่าง ๆ ของ Date (ที่จะถูก + 1900 เข้าไปอัตโนมัติ) แต่ปัญหาที่สำคัญที่สุดอย่างการเป็น สองคลาสในคลาสเดียว ก็ยังคงอยู่

สองคนในร่างเดียว ?

ทั้ง Date และ Caleandar นั้นเป็นคลาสที่จริง ๆ แล้วเป็น wrapper ครอบเวลาของเครื่องคอมพิวเตอร์ที่มันทำงานอยู่ โดยตัวข้อมูลข้างในนั้นจริง ๆ เป็นตัวเลขที่นับจำนวนมิลลิวินาทีนับตั้งแต่เที่ยงคืนของวันที่ 1 มกราคม 1970 ตัว method ที่เอาไว้หาว่า ตอนนี้วันที่เท่าไหร่ เดือนอะไร ปีอะไร กี่โมง กี่นาที กี่วินาที นั้นจะคำนวนว่าไอ้เจ้าค่าที่เก็บไว้นั้นมันผ่านมาจากเวลาข้างต้นมาเท่าไหร่ แล้วคืนค่ากลับออกมา

ธรรมชาติของคลาสกลุ่มนี้ คือ มันจะเก็บทั้งวันที่ และเวลา ลงไปในคลาสเดียวนั่นเอง

แต่ในมุมมองของผู้ใช้คลาส พอเราเห็นชื่อคลาส เราก็จะนึกว่า เอ๊ะนี่มันคือ "วันที่" นะ (เพราะปฎิทินมันไม่มีเวลาบอก) ดังนั้นเราก็จะคาดหวังว่า ถ้าเรารันโปรแกรมข้างล่าง ในวันที่ 29 มกราคม 2016 แล้วมันควรจะคืนค่า true บอกว่าเป็นวันนี้แหละ

public bool IsToday2016Jan29() {
  Calendar c = new GregorianCalendar(2016, Calendar.JANUARY, 29);
  Calendar today = Calendar.getInstance();
  return today.equals(c);
}

method นี้จะมีโอกาส return true ได้แค่ 1 วินาทีเท่านั้น (ไม่ใช่ทั้งวัน) และเอาเข้าจริง ๆ ต่อให้เราเช็คแต่ละฟิล์ดว่าเป็นวันเดียวกันหรือไม่ ก็มีโอกาสจะไม่ตรงกันได้อีก ขึ้นอยู่กับ timezone ของตัวแปรทั้งสองตัว ...

เดือน ...

อันนี้เป็นบั๊กที่คนเขียน Java แทบทุกคนโดนมาแล้วกับตัว ... ลองดูโค๊ดข้างล่างนี้นะครับ ...

// create a calendar with date = January 30, 2015
Calendar c = new GregorianCalendar(2015, 1, 30);

มีใครบอกได้ไหมว่าทำไมมันถึงพังตรงนี้ ? คำตอบคือ ถ้าเราไปดูใน Java Doc เราจะพบว่า เดือนมกราคมมีค่าเป็น 0 และไอ้ค่าเดือน 1 นั่นน่ะครับมันคือ February ... ซึ่งไม่มีวันที่ 30

นี่ยังโชคดี แค่รันแล้วเจอ exception ทุกรอบ ถ้าเกิดว่าเป็นวันอื่น ๆ เราก็จะไปรู้ตัวอีกทีตอนผลลัพท์ผิด นั่นน่ากลัวกว่าเยอะครับ

โค๊ดที่ถูกต้องสำหรับโจทย์ด้านบน คือ

Calendar c = new GregorianCalendar(2015, Calendar.January, 30);

สำหรับเราคนไทย เราใช้เดือนเป็นชื่อ ไม่ว่าทั้งภาษาไทยและภาษาอังกฤษ คนฝรั่งส่วนใหญ่ก็ยังพอจะโชคดี เพราะใช้ชื่อเดือนเหมือนกัน แต่ความซวยมันไปตกที่คนญี่ปุ่นครับ เพราะว่าในภาษาญี่ปุ่น มันไม่มีชื่อเดือนครับ พวกเขาจะเรียก เดือนหนึ่ง (一月 ishigatsu) เดือนสอง (二月nigatsu) เดือนสาม (三月 sangatsu) อะไรแบบนี้ ถ้าจำไม่ผิด ภาษาเกาหลีและจีนก็เป็นแบบนี้เช่นกัน

อีกอย่างคือนี่คือการอ้างอิงปฎิทิน Gregorian ที่มี 12 เดือน แต่ ไม่ใช่ทุกปฎิทินจะมี 12 เดือน (ปฎิทินแบบฮิบบรูวจะมี 13 เดือนในบางปี ส่วนแบบเขมรนั้นบางปีจะมีเดือน 8 สองเดือน!) ดังนั้นนี่เป็นอีกจุดที่เป็นปัญหาในการออกแบบคลาสชุดนี้

ดังนั้น เพื่อให้ระบบเดือนครอบคลุมหลายๆ ภาษา และหลายๆ ปฎิทิน การเรียกเดือนนั้นจะเรียกเป็นตัวเลข นับตั้งแต่ 1 จนถึง 12 หรือ 13 แล้วแต่ปฎิทินที่ใช้ครับ ทั้งนี้การเรียกเดือนเป็นตัวเลขนั้นก็สอดคล้องกับมาตรฐาน ISO8601 ด้วยครับ แต่มาตรฐานนี้เขาใช้ Gregorian เป็นฐานน่ะนะ

interface สุดพิศดาร

คลาส Date มีลักษณะพิเศษคือมันเป็น Immutable Class ก็คือ เมื่อสร้างเสร็จแล้วใครจะไปเปลี่ยนอะไรมันไม่ได้ เมื่อสร้างเสร็จแล้ว ถ้าเกิดจำเป็นจะต้องเปลี่ยนวันที่ใหม่ ทางเดียวที่ทำได้คือ ... สร้าง object ใหม่ครับ

สำหรับ Calendar นั้นดีกว่าตรงที่มันยังพอจะเป็น Mutable Class ที่เรายังพอเปลี่ยนค่าข้างในได้บ้าง แต่ด้วยความที่มันเป็นปฎิทิน มันถูกออกแบบมาให้รองรับกับปฎิทินหลาย ๆ แบบ ดังนั้นแทนที่มันจะมี getDate(), getYear() หรืออะไรแบบนี้ พี่ท่านดันมาพร้อมกับ method เดียว นั่นคือ get(int field) เป็นอะไรที่น่าหงุดหงิดมาก เพราะเวลาเราเขียนเราก็จะคิดว่าเฮ้ยมันน่าจะมีเมธอดนี้นะ ปัญหาอีกอีกอย่างคือถ้าใส่ตัว input ผิดโปรแกรมก็พัง ... แถม input ดันเป็น int ธรรมดาอีก (ต้องเข้าใจว่ายุคนั้นเรายังไม่มี enum นะครับ)

คือพูดกันตรง ๆ เลยว่า Calendar เนี่ยเป็นอะไรที่น่ารำคาญมากเวลาใช้ ...

ปีใหม่ใหม่

สำหรับท่านที่อ่านถึงตรงนี้แล้ว ขออนุญาตพักเบรค ขอเสนอเพลงที่ว่า "ปีใหม่ใหม่" จากคุณโรส สิรินทิพย์นะครับ

ชีวิตยังมีความหวังในปีใหม่ใหม่ ...

ที่ผมจะบอกคือใน Java 8 มีการเพิ่ม Date Time API ตัวใหม่ในแพคเกจที่ชื่อว่า java.time ซึ่งเป็นผลจาก JSR-310 ซึ่ง กว่าจะได้ใช้กันก็ต้องรอกันเป็นสิบปี (ตัว JSR ตัวนี้เริ่มต้นตั้งแต่ปี 2007 ครับ น่าจะประมาณยุค Java 6)

คลาสชุดนี้เป็นการแยกเอาไอ้เจ้าคลาสครอบจักรวาลข้างบนมาแตกออกให้กลายเป็นคลาสเล็กคลาสน้อยยิบย่อย ซึ่งเราสามารถเลือกมันขึ้นมาใช้ได้ตามความเหมาะสมครับ โดยคลาสหลัก ๆ ก็จะมีดังนี้

  • LocalDate เป็นคลาสที่เก็บวันที่โดยไม่มีเวลามาเกี่ยวข้อง คือเก็บแค่ วัน เดือน ปี แค่นั้น ไม่มีเวลา ไม่มีไทม์โซน และอื่น ๆ
  • LocalTime เป็นคลาสที่มีแต่เวลา ไม่มีวันที่ มีแค่ ชั่วโมง นาที วินาที แค่นั้น ไม่มีไทม์โซนด้วยครับ
  • LocalDateTime ก็ตามชื่อ คือมีแค่ วัน เดือน ปี และ ชั่วโมง นาที วินาที ไม่มีไทม์โซน
  • ZonedDateTime อันนี้จะมีไทม์โซนเข้ามาเกี่ยวแล้วครับ
  • OffsetDateTime คล้าย ๆ ข้างบน แต่จะเป็นการระบุ offset แทนที่จะระบุ timezone คือ มีหลาย timezone ที่มี offset เท่ากันครับ ถ้าเกิดว่าตัวชื่อ timezone ไม่สำคัญ เราก็สามารถใช้ OffsetDateTime ได้

นอกจากนี้ java.time ยังมีคลาสที่ใช้ในการคำนวนหาระยะเวลานับจากจุดหนึ่งไปยังอีกจุดหนึ่งของแกนเวลา (เท่ห์ไหม ?) อีกด้วย

  • Period เป็นระยะเวลานับเป็นวัน สามารถคำนวนหาเป็นเดือน หรือปีได้ด้วย
  • Duration เป็นระยะเวลานับเป็นวินาที สามารถเอาไปคำนวนเป็นระยะอื่น ๆ ได้เช่นกัน

การคำนวนเวลาทำได้ง่ายขึ้นมากครับ เพราะว่าเราไม่จำเป็นต้องเอาไอ้คลาสสองคลาสมาทำเป็นทุกสิ่งทุกอย่างแล้ว โค๊ดก็เขียนง่ายและอ่านง่ายขึ้น อย่างไอ้ฟังก์ชันที่หาว่าวันที่ที่ใส่เข้ามาเป็นวันนี้หรือเปล่า เราสามารถยุบเหลือ date.equals(LocalDate.now()) ได้เลย

ส่วนเรื่องเดือนก็แก้แล้วเหมือนกัน คลาสชุดนี้ใช้เดือน 1-12 ครับ อย่าง January 30, 2015 ก็เขียนว่า

LocalDate date = LocalDate.of(2015, 1, 30) 

ไม่มีการหลงละว่า เดือน 1 คือเดือนอะไรกันแน่

ความเหนือชั้นอีกอย่งของ java.time คือคลาสในชุดนี้มีชื่อที่สือความหมายว่าตัวมันคืออะไร ในขณะที่ java.util.Date และ java.util.Calendar นั้นมีชื่อที่ไม่ได้สื่อถึงการออกแบบและวิธีใช้งานอย่างชัดเจน ทำให้คนใช้กันแบบผิด ๆ

สำหรับคนที่ 'ปีใหม่ใหม่' ยังมาไม่ถึง

สำหรับคนที่ยังคงติดอยู่กับวังวนของ Java 7 นะครับ อันที่จริงก่อน JSR-310 จะถูกสร้างขึ้น ก็มี library ตัวหนึ่งที่ชื่อว่า Joda-Time ที่ออกแบบมาเพื่อสู้รบกับความห่วยของ Date และ Calendar (อันที่จริงจะโทษสองคลาสนี้ก็ไม่ถูกนัก แค่ว่าชื่อมันไม่สื่อถึงสิ่งที่มันเป็นเท่านั้นเอง) Joda-Time ได้รับความนิยมสูงมาก ใน mvn central repository นั้นมีบางเวอร์ชันที่ถูกนำไปใช้งานมากกว่า 450 artifact (ก็ประมาณว่าซอฟต์แวร์ + เวอร์ชันน่ะครับ

Joda-Time มีลักษณะใกล้เคียงกับ Java 8 Date/Time API มากทีเดียว ชื่อคลาสก็ใกล้เคียงกัน คอนเซพท์ก็เหมือนกัน ดังนั้นเรียนรู้ไปไม่เสียหายครับ

ทั้งนี้ผู้พัฒนา Joda-Time แนะนำว่า ถ้าเป็นไปได้ควร migrate โค๊ดไปใช้ Java 8 Date/Time ไปเลยดีกว่าถ้าทำได้ (เข้าใจว่าคงไม่อยากดูแลแล้ว เพราะมีตัวที่ทำงานได้ใกล้เคียงกันมาในตัว standard library เรียบร้อยแล้ว)

ก็ลองศึกษากันดูนะครับ

Wutipong Wongsakuldej

Programmer, interested in frontend applications, music and multimedia.

Latest posts by Wutipong Wongsakuldej (see all)

Leave a Reply

Your email address will not be published. Required fields are marked *