อ่าน Effective Java ตอนที่ 5 (หลีกเลี่ยงการสร้าง Object ที่ไม่จำเป็น)

ห่างหายไปสักพักแล้ว ไม่ได้เขียนเลยครับ เพราะว่าไปอยู่บนเขาบนดอยมาหลายวัน วันนี้กลับมาก็กลับมาเขียนสักหน่อย

วันนี้กลับมาเขียนเรื่อง หลีกเลี่ยงการสร้าง Object ที่ไม่จำเป็น

หลายๆครั้ง เรามักจะใช้ Object เดียวกัน แทนที่จะสร้างขึ้นมาใหม่ทุกครั้งที่ต้องการใช้ การใช้ Object เดิมนั้น ทั้ง ไวกว่า และ สละสลวยกว่า ด้วย (เขาเขียนมาแบบนี้)

ตัวอย่างต่อไปนี้เป็นอะไรที่ไม่ควรทำอย่างยิ่ง

String s = new String("stringette"); // DON'T DO THIS!

การทำแบบนี้ทำให้มันสร้าง String Instance ขึ้นมาใหม่ ทุกๆครั้งที่ถูกเรียก และ พวกมันไม่จำเป็นต้องสร้างเลยด้วยซ้ำ argument ใน constructor นั้น (“stringette”) ตัวมันเองก็เป็น String อยู่แล้วนิ แล้วยังมาถูกสร้างโดย constructor อีก ถ้าการเรียกแบบนี้อยู่ใน loop หรือ method ที่ถูกเรียกบ่อยๆแล้ว มันคงสร้าง String ออกมาเป็นล้าน โดยไม่จำเป็น

มันจะดีกว่าถ้าเราเขียนแบบนี้ใช่มะ

String s = "stringette";

 

การทำแบบนี้ทำให้มันไม่สร้าง String ขึ้นมาซ้ำซ้อนกัน  สร้างมาแค่ตัวเดียว และวิธีนี้ยังเป้นการการันตีด้วยว่า Object จะได้รับการ reuse ถ้าตลอดไม่ว่าจะเป็น code ไหนใน บน virtual machine ที่ใช้การเรียกแบบนี้เหมือนกัน

หลายๆครั้ง การหลีกเลี้ยงการสร้าง Object ที่ไม่จำเป็นก็สามารถใช้ Static Factory Method ได้เหมือนกัน (ตั้งแต่ตอนที่ 1 เลยนะ)

ในบางครั้งการ reuse Object ที่ไม่มีการเปลี่ยนแปลง (Immutable Object) นั้น เราสามารถใช้การ reuse Object ที่มีการเปลี่ยนแปลงได้ (Mutable Object) ถ้าเรารู้ว่าพวกมันไม่สามารถเปลี่ยนแปลงได้ (งงอะเดะ)

ยกตัวอย่างแล้วกัน ถ้าเกิดว่าเราต้องการสร้าง class Person ที่มี Date Object ที่สามารถเปลี่ยนแปลงได้ แต่เรารู้ว่ามันจะไม่มีการเปลี่ยนแปลงแน่ๆหลังจากเราคำนวนแล้ว โดยใน class นี้มี method ที่ชื่อว่า isBabyBoomer ที่จะบอกว่าคนคนนี้เกิดในช่วง 1946 ถึง 1964 หรือป่าว โดยตัวอย่างแบบแย่ๆก็จะเป็นแบบนี้

public class Person {
    private final Date birthDate;
    // Other fields, methods, and constructor omitted

    // DON'T DO THIS!
    public boolean isBabyBoomer() {
        // Unnecessary allocation of expensive object
        Calendar gmtCal =
                Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        Date boomStart = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        Date boomEnd = gmtCal.getTime();
        return birthDate.compareTo(boomStart) >= 0 &&
                birthDate.compareTo(boomEnd) < 0;
    }
}

มันไม่ดียังไง มันไม่ดีเพราะทุกครั้งที่เรียก isBabyBoomer() มันจะสร้าง Calendar, TimeZone แล้วก็ Date 2 อัน แต่ถ้าเราจะเขียนแบบหลีกเลี่ยงเหตุการณ์เหล่านั้นหล่ะก็

class Person {
    private final Date birthDate;
    // Other fields, methods, and constructor omitted
    /**
     * The starting and ending dates of the baby boom.
     */
    private static final Date BOOM_START;
    private static final Date BOOM_END;
    static {
        Calendar gmtCal =
                Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_START = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_END = gmtCal.getTime();
    }
    public boolean isBabyBoomer() {
        return birthDate.compareTo(BOOM_START) >= 0 &&
                birthDate.compareTo(BOOM_END) < 0;
    }
}

โดยถ้าเขียนแบบนี้ มันจะมีการสร้าง Calendar, TimeZone และ Date ครั้งเดียวเท่านั้น ตอนที่มันถูกสร้าง แทนที่จะสร้างใหม่ ตลอดๆทุกครั้งที่เรียก isBabyBoomer() โดยถ้าให้เทียบว่าดีกว่าเยอะไหม คนเขียนเขาบอกว่า เรียก 10ล้านครั้ง แบบเก่าใช้เวลา 32 วินาที แต่ถ้าใช้แบบที่ดีกว่าใช้เวลาเพียง 130 เสี้ยววินาทีเท่านั้น ซึ่งมันไวกว่าถึง 250 เท่าเลยทีเดียว ซึ่งนอกจากเร็วกว่าแล้ว จาก boomStart และ boomEnd เปลี่ยนเป็นค่าคงที่แล้ว ทำให้ชัดเจนเลยว่ามันจะไม่มีการเปลี่ยนแปลง และทุก Object ใช้เหมือนกัน

แต่ถ้าเกิดว่า Person ไม่มีการเรียกใช้ isBabyBoomer() เลยสักครั้งแล้ว BOOM_START กับ BOOM_END จะถูกสร้างมาโดยไม่จำเป็นเลย ซึ่งแก้ไขได้ด้วยการทำ Lazily initializing (ซึ่งยังไม่ต้องไปรู้จักมันก็ได้นะ) ในครั้งแรกที่เรียก isBabyBoomer() แต่วิธีนี้ก็ไม่แนะนำ เพราะว่าถ้ามีการเรียกหลายๆครั้งแล้ว มันอาจจะมีการเรียกที่ยุ่งยากในนั้นเพิ่มขึ้นซึ่งเราอาจจะเขียนผิด หรือ เราอาจจะสับสนจาก algorithm ยากๆก็ได้ ซึ่งประสิทธิภาพก็อาจจะไม่ได้ดีขึ้นมากเท่าไหร่

หลังจาก release 1.5 มา ก็มีวิธีการสร้าง Object ที่ไม่พึงประสงค์ หรือไม่จำเป็นแบบใหม่ขึ้นมา เรียกกันว่า Autoboxing มันคือการที่ทำให้ Programmer หรือ Developer สามารถใช้ Primitive และ Boxed primitive (long กับ Long หรือ int กับ Integer) ปนกันโดยมันจะ boxing และ unboxing ให้ โดยอัตโนมัติถ้าจำเป็น

โดยตัวอย่าง โปรแกรมต่อไปนี้จะทำการหาผลบวกของ int ที่เป็นบวกทั้งหมด โดยโปรแกรมที่เขียน เป็นแบบนี้

// Hideously slow program! Can you spot the object creation?
public static void main(String[]args){
    Long sum=0L;
    for(long i=0;i<Integer.MAX_VALUE;i++){
        sum+=i;
    }
    System.out.println(sum);
}

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

ที่มันช้าก็เพราะ sum ถูกประกาศให้เป็น Long แทนที่จะเป็น long ซึ่งแปลว่าโปรแกรมนี้ได้สร้าง Long Instance ที่ไม่จำเป็นออกมาจำนวน 231 ตัว ซึ่งการเปลี่ยนจาก Long เป็น long นั้นทำให้ Runtime ลดลงจาก 43 วินาที เหลือเพียง 6.8 วินาทีเท่านั้น(ในเครื่องของผู้เขียนหนังสือ) เพราะฉะนั้นแล้ว จงเลือกใช้ primitives แทนที่จะใช้ boxed primitives และ ระวังการ autoboxing ที่เราไม่ตั้งใจ ด้วยครับ

ในทางตรงกันข้ามการหลีกเลี่ยงการสร้าง object โดยการทำ Object pool ของตัวเองนั้น เป็นแนวคิดที่ไม่ค่อยดี นอกเสียจากว่า Object ภายใน pool นั้น มันมีขนาดใหญ่มากๆ โดยตัวอย่างที่ใช้กันบ่อยๆก็คือการสร้าง Object pool ของ Database connection เพราะการสร้าง connection กับ Database แบบอื่นๆค่อนข้างที่จะใช้ทรัพยากรอย่างมาก หรือบางทีเราไปใช้ database ของคนอื่นที่มีการเข้าถึงได้ไม่กี่ connection ก็เลยต้อง limit ไว้

ซึ่งจริงๆในบทหลังๆก็จะมีการพูดถึงเรื่องนี้อีกครั้งในทางกลับกับ แค่เปลี่ยนจาก “อย่าสร้างใหม่ถ้าเราควรจะเอาอันเก่าที่มีอยู่กลับมาใช้” เป็น “อย่าใช้อันเก่าที่มีอยู่ถ้ามันควรจะสร้างใหม่”

Comments