toString : เขียนไปทำไม ?คิด : สงสัยจังครับว่าเราจะเขียนเมท็อด toString ให้กับคลาสของเรา้ทำไม ? พ่อ : ก่อนอื่นขอยกตัวอย่างที่แสดงให้เห็นถึงข้อดีของ toString สมมติเรามีข้อมูลเก็บใน ArrayList จำนวนหนึ่ง แล้วต้องการแสดงออกมาดูระหว่างการทำงาน เพื่อตรวจสอบความถูกต้อง ก็เพียงแต System.out.print ออปเจกต์ของ ArrayList ที่สนใจได้เลย ดังตัวอย่างข้างล่างนี้ 01: import java.util.LinkedList;ลองแปลและสั่งทำงาน ก็จะได้ผลดังนี้ (อาจได้ผลต่างจากนี้ก็ได้เพราะเราสร้างข้อมูลสุ่ม) [9, 7, 5, 2, 5] บรรทัดที่ 6-8 สร้างข้อมูลสุ่มไปเก็บใน list บรรทัดที่ 9 แสดง list ออกจอภาพ บรรทัดที่ 9 นี้เขียนง่าย แต่สิ่งที่เกิดขึ้นจริงๆ นั้นจะมีหลายขั้นตอน เมท็อด print นั้นรับพารามิเตอร์หลากหลายชนิด สำหรับบรรทัดที่ 9 นี้เป็นการส่งออปเจกต์ไปให้ ซึ่งตรงกับหัวเมท็อด public void print(Object x) เมท็อดนี้มีรายละเอียดดังนี้ public void print(Object obj) {ซึ่งก็ไปเรียกเมท็อด valueOf ของคลาส String ซึ่งมีรายละเอียดดังนี้ public static String valueOf(Object obj) {ก็จะเห็นได้เลยว่าเขาไปเรียกเมท็อด toString ของออปเจกต์ที่ได้รับ ได้ผลคืนกลับไปให้เมท็อด write แสดงออกจอภาพ คิด : นี่ย่อมแสดงว่าในตัวอย่างที่แล้ว คำสั่ง System.out.print(list) มีผลให้ไปเรียก toString ของ list ซึ่งให้ผลคือ " [9, 7, 5, 2, 5]" เนื่องจาก list เป็นออปเจกต์ของคลาส LinkedList ผมอยากรู้ครับว่าเมท็อด toString ของ LinkedList เป็นอย่างไร ? พ่อ : ก็ไปเอามาดูก็จะรู้ (ตรงนี้ต้องบอกก่อนว่าจริงๆ แล้วในคลาส LinkedList ไม่มี toString หรอก แต่เนื่องจาก LinkedList ไปรับมรดกเมท็อด toString ของคลาส AbstractCollection ซึ่งเป็นคลาสปู่ทวด) 467: public String toString() {เมท็อดนี้เตรียม StringBuffer ไว้เก็บผลลัพธ์ (บรรทัดที่ 468) เริ่มด้วยตัว "[" (บรรทัดที่ 470) จากนั้นขอใช้ iterator (บรรทัดที่ 472) เพื่อไล่ดึงข้อมูล (บรรทัดที่ 476) ออกมาเพิ่มใน stringbuffer (โดยการเรียกใช้ String.valueOf เพื่อแปลงออปเจกต์แต่ละตัวใน list มาแปลงเป็นสตริง) แล้วปิดท้ายด้วย "]" (บรรทัดที่ 485) ก่อนที่จะแปลง stringbuffer นี้กลับเป็นสตริง (บรรทัดที่ 486) ซึ่งก็ใข้เมท็อด toString เข่นกัน คิด : แต่ทำไมเวลาผมส่งอาเรย์ไปให้ System.out.print เขากลับไม่ได้แสดงข้อมูลในอาเรย์ แต่แสดงค่าอะไรก็ไม่รู้แปลกๆ ? พ่อ : ดูตัวอย่างข้างล่างนี้ 1: public class B {หลังจากแปลและสั่งทำงานจะได้ข้อความ [I@df6ccd แสดงออกจอภาพ ซึ่งดูแปลกๆ ไม่มีความหมายอะไรเลย สิ่งที่เกิดขึ้นก็เหมือนกับที่แสดงไว้ก่อนหน้านี้ เราเรียก System.out.print โดยส่งอาเรย์ไป ก็จะไปตรงกับเมท็อดเดิมที่รับออปเจกต์ เพราะในจาวานั้นอาเรย์ประเภทใดก็ตามถือว่าเป็นออปเจกต์อย่างหนึ่ง สำหรับอาเรย์นั้นเป็นออปเจกต์ของคลาสที่มีชื่อแปลกๆ ใช้ภายในระบบเท่านั้น (สำหรับตัวอย่างข้างบนนี้ อาเรย์ของ int เป็นออปเจกต์ของคลาื่สชื่อ [I ถ้าเป็นอาเรย์ของ float ก็ [F ลองเดากรณีอื่นดูสิ) สิ่งที่แตกต่างกับตัวอย่างของ LinkedList ก็คือ คลาส LinkedList มี toString ซึ่งคืนสตริงที่แทนข้อมูลต่างๆ ภายใน list ในขณะที่ไม่มีเมท็อด toString ของอาเรย์ แต่เนื่องจากจาวาถือว่าอาเรย์เป็นออปเจกต์ขอ คลาส Object ด้วยและก็มีเมท็อด toString ในคลาส Object ดังนั้นการเรียก toString กับอาเรย์จึงเป็นการเรียก toString ของ Object ซึ่งมีรายละเอียดการทำงานดังนี้ public String toString() {ถึงตรงนี้ก็คงพอจะอธิบายได้แล้วนะว่าทำไมบรรทัดที 7 System.out.print(a) จึงได้ [I@df6ccd เมท็อด toString ของ Object เรียก getClass().getName() ได้สตริง "[I" คืนมาต่อกับ "@" ตบท้ายด้วยสตริงซึ่งแทนจำนวนฐานสิบหกของผลจากการเรียกเมท็อด hashCode คงจะไม่อธิบายตรงนี้นะว่า hashCode คืออะไร เอาเป็นว่าสำหรับกรณีที่เกิดขึ้นนี้ hashCode คืนตำแหน่งเริ่มต้นของหน่วยความจำที่เก็บตัวอาเรย์ในที่นี้มีค่า df6ccd (ถ้าลองสั่งทำงานใหม่ ก็ไม่แน่ว่าจะได้ค่านี้ เพราะระบบอาจเก็บอาเรย์นี้ไว้ตำแหน่งอื่นก็เป็นได้) คิด : พ่อก็ยังไม่ได้ตอบคำถามผมเลยว่า เราจะเขียน toString ไปทำไม ? พ่อ : เอาล่ะ จากตัวอย่างที่ผ่านมา เราเห็นได้ว่า ถ้าเรามี toString ที่เปลี่ยนข้อมูลหรือสภาวะภายในออปเจกต์ออกมาเป็นสตริงที่คนอ่านรู้เรื่อง ก็จะมีประโยชน์มากต่อการหาที่ผิด หรือตรวจสอบความถูกต้องของการทำงานที่ต้องใช้ตาและสมองของโปรแกรมเมอร์ เมื่อใดที่โปรแกรมเมอร์อยากรู้ค่าของออปเจกต์หนึ่งระหว่างการทำงาน ก็สามารถพิมพ์ออปเจกต์นั้นออกมาดูได้่ง่ายๆ (เพราะระบบจะเรียก toString ให้) debugger โดยทั่วไปก็จะเรียก toString ให้ ถ้าเราต้องการ watch ออปเจกต์ที่สนใจ แต่ถ้าออปเจกต์ที่เราอยากดูไปใช้ toString ของ Object ก็น่าอึดอัด เพราะดูไม่รู้เรื่อง และถ้า toString จัดรูปแบบของสตริงที่คืนกลับให้อ่านรู้เรื่องได้ใจความ ก็สามารถนำไปแสดงเป็นผลลัพธ์หนึ่งของโปรแกรมได้ด้วยเช่นกัน เช่น toString ของ java.util.Date คืนสตริงที่เอาไปแสดงได้เลยในรูปแบบวันเดือนปีและเวลาตามสากล เช่น 01: import java.awt.*; 02: import java.applet.*;แปลและสั่งทำงานจะได้โปรแกรมนาฬิกาข้างล่างนี้ String s = "this is " + new java.awt.Point(2,3);จะได้ผลของ s เป็น "this is java.awt.Point[x=2,y=3]" คิด : ผมเข้าใจถูกไหมว่า เราเขียน toString ให้กับคลาส A ก็เพื่อจะได้หาข้อผิดพลาดของเมท็อดต่างๆ ของคลาส A พ่อ : ผิดๆๆ จุดมุ่งหมายหลักของการมี toString ให้กับคลาส A ก็เพื่อเป็นประโยชน์สำหรับผู้อื่นที่ใช้คลาส A (แน่นอนว่าก็เป็นประโยชน์สำหรับการพัฒนาเมท็อดต่างๆ ของคลาส A ด้วย) ถ้าเราเขียนโปรแกรมจาวาที่ใช้คลาสอื่นๆ อยู่เรื่อยๆ มันติดเป็นนิสัยว่าถ้าอยากดูค่าต่างๆ ภายในออปเจกต์ ก็ส่งออปเจกต์นั้นออกไป print เลย mี่ติดเป็นนิสัยก็เพราะว่าคลาสอื่นๆ ที่คนอื่นๆ เขาเขียนกัน มีบริการ toString ให้ทั้งนั้น ถ้าเราเขียนคลาสใหม่ ที่คิดว่าเป็นประโยชน์สำหรับการไปใช้ใหม่ ในคลาสอื่นๆ (ไม่ว่าจะเป็นเราเองที่ใช้ หรือผู้อื่นที่ไม่รู้จัก หรือลูกค้าที่จ่ายเงินซื้อ) ก็ควรให้บริการ toString ตามธรรมเนียมปฏิบัติ คิด : โอเคครับ พอรู้ว่ามันสำคัญ แล้วจะเขียนรายละเอียดการทำงานอย่างไรครับ ถึงจะเหมือนที่คนในวงการเขาเขียนๆ กัน พ่อ : ขอตัวไปห้องน้ำก่อน แล้วจะมาตอบให้ภายหลัง ตอนนี้ดึกแล้ว คิดไปนอนได้แล้ว เกร็ดกาแฟ (1 ม.ค. 2547) Copyright 2004
Somchai Prasitjutrakul |