Programmer Diary

ไดอารี่โปรแกรมเมอร์

Nov 2009 12 5:53 pm

ผิดแบบนี้มันน่า… ใน Java ตอนที่ 2

Share/Save/Bookmark

ต่อจากตอนที่แล้วนะครับ วันนี้จะพูดถึงความผิดพลาดเรื่องต่อไป ที่คนเขียน Java มักจะพบกันบ่อย ๆ

NullPointerException

ใครที่ไม่เคยเจอ NullPointerException แปลว่า ไม่เคยเขียน Java

คำกล่าวนี้ ออกจะดูเกินเลยไปหน่อย แต่คนที่เขียน Java ย่อมรู้ดีว่า การจะระมัดระวังไม่ให้เกิด NullPointerException นั้น เป็นเรื่องที่ค่อนข้างยาก โดยเฉพาะมือใหม่ ที่ยังไม่คุ้นกับ “ธรรมเนียม” ปฏิบัติที่คนทั่วไปนิยมใช้ เพื่อหลีกเลี่ยงปัญหานี้

ก่อนที่ใครจะรู้สึกว่า ทำไมภาษา Java ถึงได้มีปัญหามากยังงี้ ต้องบอกไว้เลยว่าการแก้ปัญหา NullPointerException นั้น ส่วนใหญ่อาศัยเพียงแค่การทำ code review หรือ debug อย่างง่าย ๆ ก็สามารถแก้ได้แล้ว เพราะสาเหตุของปัญหาส่วนใหญ่มาจากการ “ลืม” (จริง ๆ แล้ว น่าจะเรียกว่าเลินเล่อมากกว่า)

แต่ก็มีหลายครั้ง ที่ NullPointerException แก้ไม่ได้ง่าย ๆ อย่างนั้น บางครั้งทำเอาเราเสียเวลาไปไม่น้อยเหมือนกัน

หลายปีก่อน ระหว่างผมนั่งทำงานอยู่ มีน้องคนหนึ่งมาสะกิด “พี่ ๆ ช่วยมาดูโค้ดหน่อย เป็นอะไรไม่รู้ error หาสาเหตุไม่เจอ”

ใช้เวลาร่วมหลายนาที กว่าจะเจอว่าเป็น NullPointerException ที่ต้องใช้เวลานานก็เพราะ โปรแกรมทำงานบนเซิร์ฟเวอร์ และคนเขียนไม่ได้เขียน log ไว้ ดังนั้นจึงต้องไปไล่หาจาก standard output ของตัวเซิร์ฟเวอร์ ซึ่ง Exception ของเราก็ปนไปกับ output อื่น ๆ ของเซิร์ฟเวอร์ นอกจากนั้นโค้ดยังเขียนไว้ซับซ้อน ไม่เป็นระเบียบ การจะไล่ทำความเข้าใจโค้ด ก็เสียเวลาไปไม่น้อย

ปัญหา คือ เมื่อพบแล้วว่าเป็น NullPointerException ก็ควรจะแก้ได้ไม่ยาก ไม่ใช่หรือ ?

ลองดูโค้ดต่อไปนี้

PrintWriter out = null;
try {
    out = new PrintWriter(new FileWriter("D:\\test\\test.txt"));
} catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}

out.println("Hello World");
out.close();

เมื่อรันแล้วได้ผล ดังนี้

java.io.FileNotFoundException: D:\test\test.txt (The system cannot find the path specified)
	at java.io.FileOutputStream.open(Native Method)
	at java.io.FileOutputStream.(Unknown Source)
	at java.io.FileOutputStream.(Unknown Source)
	at java.io.FileWriter.(Unknown Source)
	at EmptyCatch.main(EmptyCatch.java:12)
Exception in thread "main" java.lang.NullPointerException
	at EmptyCatch.main(EmptyCatch.java:18)

จะเห็นว่า Exception ตัวสุดท้ายที่เราเจอคือ NullPointerException ซึ่งจากเลขบรรทัดที่กำกับ ถ้าเราตามไปดูที่โค้ด ก็ไม่เห็นว่ามันน่าจะผิดตรงไหน เพราะตัวแปร out ก็ initialize เอาไว้แล้ว

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

ถ้าลองพิจารณาตัวอย่างโค้ดข้างบน จะเห็นว่า ส่วนที่สร้างออบเจ็กต์ PrintWriter นั้นถูกครอบด้วย try/catch ถ้าสังเกตดี ๆ จะเห็นว่า ในส่วนของ catch นั้น เพียงแค่สั่งให้พิมพ์ stack trace ออกมาเท่านั้น ไม่ได้จัดการกับปัญหาแต่อย่างใด ดังนั้น ถ้าในกรณีที่เกิด IOException เช่น ไฟล์ที่อ้างอิงอาจจะไม่มีอยู่จริง โค้ดจะเข้ามาในส่วนของ catch ซึ่งเพียงแค่พิมพ์ stack trace แล้วทำงานต่อไป ตัวแปร out ก็จะยังคงเป็น null อยู่ เหมือนว่าไม่ได้สร้างออบเจ็กต์แต่อย่างใด

ปัญหาลักษณะนี้ มักเกิดจากการใช้ IDE ที่มีความสามารถในการแก้ไขโค้ดให้คอมไพล์ได้โดยอัตโนมัติ เช่น ถ้าเรามีโค้ดดังนี้

PrintWriter out = new PrintWriter(new FileWriter("D:\\test\\test.txt"));

Eclipse จะขึ้นสัญลักษณ์เตือนว่าโค้ดบรรทัดนี้คอมไพล์ไม่ผ่าน เพราะคอนสตรัคเตอร์ของ FileWriter นั้น throw IOException ซึ่งเราต้อง handle ด้วยวิธีใดวิธีหนึ่ง แต่เราเพียงแค่คลิกเมาส์เพียงสองสามครั้ง เราก็สามารถสั่งให้ Eclipse แก้ไขโค้ดของเราได้เป็นดังนี้

try {
    PrintWriter out = new PrintWriter(new FileWriter("D:\test\test.txt"));
} catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}

ซึ่งถึงแม้จะคอมไพล์ได้ผ่าน แต่พฤติกรรมการทำงานนั้น ไม่เหมาะสม และไม่ใช่สิ่งที่เราต้องการอย่างแน่นอน

ในตัวอย่างนี้ จะเห็นว่า ตำแหน่งของต้นเหตุของปัญหา กับตำแหน่งที่เกิด Exception ให้เราเห็นนั้นไม่ห่างกันมาก เพียงเราไล่โค้ดย้อนกลับมาไม่กี่บรรทัดก็เจอสาเหตุแล้ว แต่ถ้าระยะห่างระหว่างสองตำแหน่งนี้มีมากขึ้น ในโปรแกรมขนาดใหญ่ โค้ดจะถูกแบ่งออกเป็นคลาส เป็นเมท็อด ที่ไม่ได้วางเรียงไว้ต่อเนื่องกัน การจะไล่หาต้นเหตุของปัญหาอาจจะไม่ง่ายเหมือนในตัวอย่างนี้ ลองคิดว่า ถ้าตำแหน่งที่เป็นต้นเหตุอยู่ในคนละคลาส คนละเมท็อด และตัวแปรที่มีปัญหาถูกส่งผ่านมาทางพารามิเตอร์ของเมท็อดหลาย ๆ ต่อ ปัญหานี้คงทำเอาเราเสียเวลาไปไม่น้อยแน่

ต้องยอมรับว่า ปัญหาลักษณะนี้แก้ได้ยาก โดยเฉพาะสำหรับมือใหม่ แต่มีวิธีสังเกตเพื่อป้องกันอย่างง่าย ๆ คือ เมื่อใดก็ตามที่มี catch ควรจะดูให้แน่ใจว่าโค้ดใน catch ได้จัดการกับ Exception อย่างเหมาะสมแล้ว บางทีการเลือกที่จะไม่จัดการ Exception โดยการปล่อยให้ Exception ถูก throw ออกไปจน terminate โปรแกรม อาจจะดีเสียกว่าการกลบซ่อน Exception เอาไว้ แล้วปล่อยให้โปรแกรมทำงานต่อไปแบบผิด ๆ

อันที่จริง ปัญหาลักษณะนี้ป้องกันได้ไม่ยากเลย เพียงเราเขียนโค้ดทุกส่วนด้วยความมีสติและระมัดระวัง อย่าปล่อยให้เครื่องมือสร้างโค้ดที่เราไม่ได้รับรู้ว่าทำงานยังไง … ซึ่งเรื่องเหล่านี้ ไม่ได้ยากเกินไปสำหรับโปรแกรมเมอร์มืออาชีพเลย

No Comments to “ผิดแบบนี้มันน่า… ใน Java ตอนที่ 2”

RSS for this post's comments

Leave a comment

not published

Recent Posts