toString : เขียนไปทำไม ?


คิด : สงสัยจังครับว่าเราจะเขียนเมท็อด toString ให้กับคลาสของเรา้ทำไม ?

พ่อ : ก่อนอื่นขอยกตัวอย่างที่แสดงให้เห็นถึงข้อดีของ toString  สมมติเรามีข้อมูลเก็บใน ArrayList จำนวนหนึ่ง แล้วต้องการแสดงออกมาดูระหว่างการทำงาน เพื่อตรวจสอบความถูกต้อง ก็เพียงแต System.out.print ออปเจกต์ของ ArrayList ที่สนใจได้เลย ดังตัวอย่างข้างล่างนี้
01: import java.util.LinkedList;
02:
03: public class A {
04: public static void main(String[] args) {
05: LinkedList list = new LinkedList();
06: for (int i = 0; i < 5; i++) {
07: list.add(new Integer((int)(10 * Math.random())));
08: }
09: System.out.print(list);
10: }
11: }
ลองแปลและสั่งทำงาน ก็จะได้ผลดังนี้ (อาจได้ผลต่างจากนี้ก็ได้เพราะเราสร้างข้อมูลสุ่ม)

[9, 7, 5, 2, 5]

บรรทัดที่ 6-8 สร้างข้อมูลสุ่มไปเก็บใน list  บรรทัดที่ 9 แสดง list ออกจอภาพ   บรรทัดที่ 9 นี้เขียนง่าย แต่สิ่งที่เกิดขึ้นจริงๆ นั้นจะมีหลายขั้นตอน   เมท็อด print นั้นรับพารามิเตอร์หลากหลายชนิด  สำหรับบรรทัดที่ 9 นี้เป็นการส่งออปเจกต์ไปให้  ซึ่งตรงกับหัวเมท็อด  public void print(Object x)  เมท็อดนี้มีรายละเอียดดังนี้
  public void print(Object obj) {
write(
String.valueOf(obj));
}

ซึ่งก็ไปเรียกเมท็อด valueOf ของคลาส String ซึ่งมีรายละเอียดดังนี้
  public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}

ก็จะเห็นได้เลยว่าเขาไปเรียกเมท็อด 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() {
468: StringBuffer buf = new StringBuffer();
469:
470: buf.append("[");
471:
472: Iterator i = iterator();
473: boolean hasNext = i.hasNext();
474:
475: while (hasNext) {
476: Object o = i.next();
477:
478: buf.append(o == this ? "(this Collection)" : String.valueOf(o));
479: hasNext = i.hasNext();
480: if (hasNext) {
481: buf.append(", ");
482: }
483: }
484:
485: buf.append("]");
486: return buf.toString();
487: }

เมท็อดนี้เตรียม StringBuffer ไว้เก็บผลลัพธ์ (บรรทัดที่ 468) เริ่มด้วยตัว "[" (บรรทัดที่ 470) จากนั้นขอใช้ iterator (บรรทัดที่ 472) เพื่อไล่ดึงข้อมูล (บรรทัดที่ 476) ออกมาเพิ่มใน stringbuffer  (โดยการเรียกใช้ String.valueOf เพื่อแปลงออปเจกต์แต่ละตัวใน list มาแปลงเป็นสตริง)  แล้วปิดท้ายด้วย "]" (บรรทัดที่ 485) ก่อนที่จะแปลง stringbuffer นี้กลับเป็นสตริง (บรรทัดที่ 486) ซึ่งก็ใข้เมท็อด toString เข่นกัน

คิด :  แต่ทำไมเวลาผมส่งอาเรย์ไปให้ System.out.print  เขากลับไม่ได้แสดงข้อมูลในอาเรย์ แต่แสดงค่าอะไรก็ไม่รู้แปลกๆ ?

พ่อ :  ดูตัวอย่างข้างล่างนี้
1: public class B {
2: public static void main(String[] args) {
3: int[] a = new int[5];
4: for (int i = 0; i < a.length; i++) {
5: a[i] = (int)(10 * Math.random());
6: }
7: System.out.print(a);
8: }
9: }
หลังจากแปลและสั่งทำงานจะได้ข้อความ  [I@df6ccd  แสดงออกจอภาพ  ซึ่งดูแปลกๆ ไม่มีความหมายอะไรเลย
สิ่งที่เกิดขึ้นก็เหมือนกับที่แสดงไว้ก่อนหน้านี้  เราเรียก System.out.print โดยส่งอาเรย์ไป ก็จะไปตรงกับเมท็อดเดิมที่รับออปเจกต์  เพราะในจาวานั้นอาเรย์ประเภทใดก็ตามถือว่าเป็นออปเจกต์อย่างหนึ่ง  สำหรับอาเรย์นั้นเป็นออปเจกต์ของคลาสที่มีชื่อแปลกๆ ใช้ภายในระบบเท่านั้น (สำหรับตัวอย่างข้างบนนี้ อาเรย์ของ int เป็นออปเจกต์ของคลาื่สชื่อ [I  ถ้าเป็นอาเรย์ของ float ก็ [F ลองเดากรณีอื่นดูสิ)

สิ่งที่แตกต่างกับตัวอย่างของ LinkedList ก็คือ  คลาส LinkedList มี toString ซึ่งคืนสตริงที่แทนข้อมูลต่างๆ ภายใน list  ในขณะที่ไม่มีเมท็อด toString ของอาเรย์  แต่เนื่องจากจาวาถือว่าอาเรย์เป็นออปเจกต์ขอ
คลาส Object ด้วยและก็มีเมท็อด toString ในคลาส Object  ดังนั้นการเรียก toString กับอาเรย์จึงเป็นการเรียก toString ของ Object ซึ่งมีรายละเอียดการทำงานดังนี้
  public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
ถึงตรงนี้ก็คงพอจะอธิบายได้แล้วนะว่าทำไมบรรทัดที 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.*;

03:
04: public class Clock extends Applet implements Runnable {
05: public void start() {
06: new Thread(this).start();
07: }
08: public void paint(Graphics g) {
09: g.drawString(new java.util.Date().toString(), 10, 20);
10: }
11: public void run() {
12: while (true) {
13: repaint();
14: try {
15: Thread.sleep(1000);
16: } catch (InterruptedException e) {}
17: }
18: }
19: }
แปลและสั่งทำงานจะได้โปรแกรมนาฬิกาข้างล่างนี้

อ้อ ลืมบอกไปอีกเรื่องหนึ่ง ใครๆ ก็รู้ว่าการบวกสตริงก็คือการนำสตริงสองตัวมาต่อกัน ดังนั้นถ้าเรานำสตริงตัวหนึ่งไปขอ "บวก" กับออปเจกต์หนึ่ง จะเป็นการเรียก toString() ของออปเจกต์นั้นโดยอัตโนมัติ เพื่อให้ได้ผลเป็นสตริงมาต่อกัน เช่น
  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