Best Practices จาก “Effective Java (3rd Edition)”
เนื้อหาบางส่วนจาก “Effective Java (3rd Edition) ” โดย Joshua Bloch
1. คืนค่า collection หรืออาร์เรย์ว่าง ไม่ใช่ null
ถ้าเมธอดคืนค่าเป็น Set อย่า return null; เมื่อผลลัพธ์ว่างเปล่า – ให้ใช้ return Set.of(); แทน
2. พิจารณาใช้ static factory method แทน constructor
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
ไม่ได้ดีกว่าเสมอไป แต่ในบางกรณีก็มีข้อดีหลายอย่าง:
- ข้อดีอย่างหนึ่งของ static factory method คือมีชื่อ ซึ่งต่างจาก constructor
- ไม่จำเป็นต้องสร้างอ็อบเจกต์ใหม่ทุกครั้งที่ถูกเรียก
- คืนค่าอ็อบเจกต์ที่เป็น subtype ใดก็ได้ของชนิดที่ประกาศไว้
- คลาสของอ็อบเจกต์ที่คืนค่าเปลี่ยนไปได้ในแต่ละครั้งที่เรียก ขึ้นอยู่กับพารามิเตอร์ที่รับเข้ามา
- คลาสของอ็อบเจกต์ที่คืนค่าไม่จำเป็นต้องมีอยู่ในตอนที่เขียนคลาสซึ่งมีเมธอดนั้น
ข้อเสีย:
- ข้อจำกัดหลักของการมีแต่ static factory method คือคลาสที่ไม่มี constructor แบบ public หรือ protected จะสร้าง subclass ไม่ได้
- ข้อด้อยประการที่สองของ static factory method คือโปรแกรมเมอร์หาเจอได้ยาก
3. พิจารณาใช้ builder เมื่อ constructor มีพารามิเตอร์จำนวนมาก
ทั้ง static factory และ constructor มีข้อจำกัดเหมือนกัน คือรับมือกับพารามิเตอร์ที่เป็นตัวเลือก (optional) จำนวนมากได้ไม่ดี ปัญหานี้เล่นงานบางโปรเจกต์ที่ผมเคยทำอย่างหนักเลยทีเดียว
4. บังคับคุณสมบัติ singleton ด้วย constructor แบบ private หรือชนิด enum
อะไรก็ตามที่ไม่ต้องมีหลายอินสแตนซ์ ควรสร้างขึ้นเพียงครั้งเดียว – จะด้วย @ApplicationScoped หรือ singleton pattern แบบอื่นก็ได้
5. ป้องกันการสร้างอินสแตนซ์ด้วย constructor แบบ private
บางครั้งคุณอาจอยากเขียนคลาสที่เป็นแค่ที่รวมเมธอด static และฟิลด์ static คลาสแบบนี้มีชื่อเสียงไม่ดีนัก เพราะบางคนใช้มันในทางที่ผิดเพื่อเลี่ยงการคิด ในแบบเชิงวัตถุ แต่มันก็มีประโยชน์ที่ชอบธรรมอยู่จริง
private MyStaticClass() {
// private constructor to prevent instantiation
}
6. หลีกเลี่ยงการสร้างอ็อบเจกต์ที่ไม่จำเป็น
String s = new String("bikini"); // DON'T DO THIS!
หรือ:
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
คำตอบ: โปรแกรมนี้ให้ผลลัพธ์ที่ถูกต้อง แต่ช้ากว่าที่ควรจะเป็นมาก เพราะพิมพ์ผิดไปตัวอักษรเดียว ตัวแปร sum ถูกประกาศเป็น Long แทนที่จะเป็น long ซึ่งหมายความว่าโปรแกรมสร้างอินสแตนซ์ Long ที่ไม่จำเป็นราว 2^31 ตัว (ประมาณหนึ่งตัวต่อการบวก long i เข้ากับ Long sum แต่ละครั้ง)
7. หลีกเลี่ยง finalizer และ cleaner
Finalizer คาดเดาไม่ได้ มักเป็นอันตราย และโดยทั่วไปก็ไม่จำเป็น การใช้มันอาจทำให้พฤติกรรมของโปรแกรมแปรปรวน ประสิทธิภาพแย่ และมีปัญหาเรื่องการย้ายแพลตฟอร์ม
8. ใช้ try-with-resources แทน try-finally
// try-with-resources - the best way to close resources!
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
9. พิจารณา implement Comparable
เมื่อ implement Comparable คลาสของคุณจะทำงานร่วมกับอัลกอริทึมแบบ generic และ implementation ของ collection จำนวนมากที่พึ่งพาอินเทอร์เฟซนี้ได้
10. ลดการเข้าถึงคลาสและสมาชิกให้น้อยที่สุด
ปัจจัยที่สำคัญที่สุดเพียงข้อเดียวที่แยกคอมโพเนนต์ที่ออกแบบมาดีออกจากคอมโพเนนต์ที่ออกแบบมาไม่ดี คือระดับที่คอมโพเนนต์ซ่อนข้อมูลภายในและรายละเอียดการ implement อื่น ๆ จากคอมโพเนนต์อื่น คอมโพเนนต์ที่ออกแบบมาดีจะซ่อนรายละเอียดการ implement ทั้งหมด และแยก API ออกจาก implementation อย่างชัดเจน คอมโพเนนต์จึงสื่อสารกันผ่าน API เท่านั้น และไม่รับรู้การทำงานภายในของกันและกัน แนวคิดนี้เรียกว่าการซ่อนข้อมูล (information hiding) หรือการห่อหุ้ม (encapsulation) ซึ่งเป็นหลักการพื้นฐานของการออกแบบซอฟต์แวร์
11. ในคลาส public ให้ใช้ accessor method ไม่ใช่ฟิลด์ public
บางครั้งคุณอาจอยากเขียนคลาสที่เสื่อมถอย (degenerate) ซึ่งไม่มีจุดประสงค์อื่นนอกจากรวบรวมฟิลด์ของอินสแตนซ์ไว้ด้วยกัน:
// Degenerate classes like this should not be public!
class Point {
public double x;
public double y;
}
เนื่องจากฟิลด์ข้อมูลของคลาสเหล่านี้ถูกเข้าถึงโดยตรง คลาสแบบนี้จึงไม่ได้ประโยชน์จากการห่อหุ้ม คุณเปลี่ยนรูปแบบการเก็บข้อมูลไม่ได้โดยไม่เปลี่ยน API บังคับ invariant ไม่ได้ และทำอะไรเพิ่มเติมตอนที่ฟิลด์ถูกเข้าถึงไม่ได้ โปรแกรมเมอร์สายเชิงวัตถุหัวแข็งมองว่าคลาสแบบนี้เป็นสิ่งต้องห้าม และควรแทนที่ด้วยคลาส ที่มีฟิลด์ private และ accessor method แบบ public (getter) เสมอ และสำหรับคลาสที่เปลี่ยนแปลงได้ ก็มี mutator (setter) ด้วย:
// Encapsulation of data by accessor methods and mutators
class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
}
แน่นอนว่าพวกหัวแข็งพูดถูกเมื่อเป็นเรื่องของคลาส public: ถ้าคลาสถูกเข้าถึงได้จากนอกแพ็กเกจ ก็ควรมี accessor method เพื่อรักษา ความยืดหยุ่นในการเปลี่ยนรูปแบบการเก็บข้อมูลภายในของคลาส ถ้าคลาส public เปิดเผยฟิลด์ข้อมูลออกมา ความหวังที่จะเปลี่ยนรูปแบบการเก็บข้อมูลก็หมดไป เพราะโค้ดฝั่ง client อาจกระจายไป ทั่วทุกที่แล้ว
อย่างไรก็ตาม ถ้าคลาสเป็น package-private หรือเป็น nested class แบบ private การเปิดเผยฟิลด์ข้อมูลก็ไม่ได้ผิดอะไรโดยตัวมันเอง—ตราบใดที่ฟิลด์เหล่านั้นอธิบาย abstraction ที่คลาสนำเสนอได้อย่างเพียงพอ
12. ลดความสามารถในการเปลี่ยนแปลง (mutability) ให้น้อยที่สุด
คลาส immutable ก็คือคลาสที่อินสแตนซ์ของมันแก้ไขไม่ได้ ข้อมูลทั้งหมดในแต่ละอินสแตนซ์คงที่ตลอดอายุของอ็อบเจกต์ จึงไม่มีทางสังเกตเห็นการเปลี่ยนแปลงใด ๆ ไลบรารีของแพลตฟอร์ม Java มีคลาส immutable อยู่มากมาย เช่น String, คลาส primitive แบบ boxed, BigInteger และ BigDecimal ซึ่งมีเหตุผลดี ๆ มากมาย: คลาส immutable ออกแบบ implement และใช้งานได้ง่ายกว่าคลาส mutable มีโอกาสผิดพลาดน้อยกว่า และปลอดภัยกว่า หากต้องการทำให้คลาสเป็น immutable ให้ปฏิบัติตามกฎห้าข้อนี้:
- อย่ามีเมธอดที่แก้ไข state ของอ็อบเจกต์ (เรียกว่า mutator)
-
ทำให้คลาสถูก extend ไม่ได้ วิธีนี้ป้องกันไม่ให้ subclass ที่เขียนอย่างสะเพร่าหรือมีเจตนาร้ายทำลายพฤติกรรม immutable ของคลาส ด้วยการทำตัวราวกับว่า state ของอ็อบเจกต์
เปลี่ยนไปแล้ว โดยทั่วไปการป้องกันการสร้าง subclass ทำได้ เช่น โดยประกาศคลาสเป็น
final - ทำให้ทุกฟิลด์เป็น final วิธีนี้แสดงเจตนาของคุณอย่างชัดเจนในแบบที่ระบบบังคับใช้ให้ นอกจากนี้ยังจำเป็นต่อการทำงานที่ถูกต้อง หากการอ้างอิงไปยัง อินสแตนซ์ที่เพิ่งสร้างถูกส่งจากเธรดหนึ่งไปยังอีกเธรดหนึ่งโดยไม่มีการ synchronize ตามที่ระบุไว้ใน memory model
- ทำให้ทุกฟิลด์เป็น private วิธีนี้ป้องกันไม่ให้ client เข้าถึงอ็อบเจกต์ mutable ที่ฟิลด์อ้างอิงอยู่และแก้ไขอ็อบเจกต์เหล่านั้นโดยตรง แม้ในทางเทคนิค คลาส immutable จะมีฟิลด์ public final ที่เก็บค่า primitive หรือการอ้างอิงไปยังอ็อบเจกต์ immutable ได้ แต่ก็ไม่แนะนำ เพราะจะทำให้เปลี่ยนรูปแบบการเก็บข้อมูลภายใน ในรีลีสถัดไปไม่ได้
-
รับประกันการเข้าถึงคอมโพเนนต์ mutable แต่เพียงผู้เดียว หากคลาสของคุณมีฟิลด์ที่อ้างอิงไปยังอ็อบเจกต์ mutable ต้องแน่ใจว่า client ของคลาสไม่สามารถได้การอ้างอิงไปยัง
อ็อบเจกต์เหล่านั้น อย่ากำหนดค่าเริ่มต้นของฟิลด์แบบนี้ด้วยการอ้างอิงอ็อบเจกต์ที่ client ส่งมา และอย่าคืนค่าฟิลด์นั้นจาก accessor ให้ทำสำเนาเชิงป้องกัน (defensive copy) ใน constructor, accessor และ
เมธอด
readObject
13. เลือกใช้ composition มากกว่า inheritance
การสืบทอด (inheritance) เป็นวิธีที่ทรงพลังในการนำโค้ดกลับมาใช้ใหม่ แต่ไม่ใช่เครื่องมือที่ดีที่สุดสำหรับงานเสมอไป ถ้าใช้อย่างไม่เหมาะสม จะทำให้ซอฟต์แวร์เปราะบาง การใช้ inheritance ภายในแพ็กเกจเดียวกันนั้นปลอดภัย เพราะ implementation ของ subclass และ superclass อยู่ในความดูแลของโปรแกรมเมอร์กลุ่มเดียวกัน การใช้ inheritance ก็ปลอดภัยเช่นกันเมื่อ extend คลาสที่ออกแบบและเขียนเอกสารมาเพื่อการ extend โดยเฉพาะ แต่การสืบทอดจากคลาส concrete ทั่วไปข้ามขอบเขตแพ็กเกจนั้นอันตราย
inheritance ละเมิดการห่อหุ้ม ซึ่งต่างจากการเรียกเมธอด คุณอาจคิดว่าการ extend คลาสนั้นปลอดภัย ถ้าคุณแค่เพิ่มเมธอดใหม่และไม่ override เมธอด ที่มีอยู่ แม้การ extend แบบนี้จะปลอดภัยกว่ามาก แต่ก็ไม่ใช่ไร้ความเสี่ยง หาก superclass มีเมธอดใหม่ในรีลีสถัดไป และคุณโชคร้ายที่ได้ให้ subclass มี เมธอดที่มี signature เดียวกันแต่ชนิดค่าที่คืนต่างกัน subclass ของคุณจะคอมไพล์ไม่ผ่านอีกต่อไป หากคุณให้ subclass มีเมธอดที่มี signature และชนิดค่าที่คืนเหมือนกับเมธอดใหม่ของ superclass ตอนนี้คุณก็กำลัง override มันอยู่ และต้องเจอปัญหาที่กล่าวไว้ก่อนหน้า ยิ่งไปกว่านั้น ก็น่าสงสัยว่าเมธอดของคุณจะทำตามสัญญาของเมธอดใหม่ใน superclass ได้หรือไม่ เพราะสัญญานั้นยังไม่ได้ถูกเขียนขึ้นตอนที่คุณเขียนเมธอดของ subclass
โชคดีที่มีวิธีหลีกเลี่ยงปัญหาทั้งหมดที่กล่าวมา แทนที่จะ extend คลาสที่มีอยู่ ให้คลาสใหม่ของคุณมีฟิลด์ private ที่อ้างอิงไปยังอินสแตนซ์ของคลาสที่มีอยู่นั้น การออกแบบนี้เรียกว่า composition เพราะคลาสที่มีอยู่กลายเป็นส่วนประกอบของคลาสใหม่ เมธอดของอินสแตนซ์แต่ละตัวในคลาสใหม่จะเรียกเมธอดที่ตรงกันบนอินสแตนซ์ของ คลาสเดิมที่บรรจุอยู่ แล้วคืนผลลัพธ์ วิธีนี้เรียกว่า forwarding และเมธอดในคลาสใหม่เรียกว่า forwarding method คลาสที่ได้จะแข็งแรงมั่นคง โดยไม่ พึ่งพารายละเอียดการ implement ของคลาสเดิมเลย แม้จะเพิ่มเมธอดใหม่ให้คลาสเดิม ก็ไม่ส่งผลกระทบต่อคลาสใหม่
14. เลือกใช้อินเทอร์เฟซมากกว่าคลาส abstract
Java มีกลไกสองแบบสำหรับนิยามชนิดที่อนุญาตให้มีหลาย implementation ได้แก่ อินเทอร์เฟซและคลาส abstract ตั้งแต่ Java 8 นำ default method มาใช้กับอินเทอร์เฟซ ทั้งสองกลไก ก็ให้คุณเขียน implementation ของเมธอดอินสแตนซ์บางตัวได้
ความแตกต่างสำคัญคือ คลาสที่จะ implement ชนิดที่นิยามโดยคลาส abstract ต้องเป็น subclass ของคลาส abstract นั้น เนื่องจาก Java อนุญาตให้สืบทอดได้ทางเดียว ข้อจำกัดนี้ของ คลาส abstract จึงจำกัดการใช้เป็นนิยามชนิดอย่างมาก คลาสใดก็ตามที่นิยามเมธอดที่จำเป็นครบทั้งหมดและปฏิบัติตามสัญญาทั่วไป สามารถ implement อินเทอร์เฟซได้ ไม่ว่าคลาสนั้นจะอยู่ตรงไหนในลำดับชั้นของคลาส คลาสที่มีอยู่แล้วก็ปรับให้ implement อินเทอร์เฟซใหม่ได้ง่าย
- อินเทอร์เฟซเหมาะอย่างยิ่งสำหรับนิยาม mixin
- อินเทอร์เฟซช่วยให้สร้างกรอบโครงของชนิดที่ไม่เป็นลำดับชั้นได้
- อินเทอร์เฟซช่วยให้เพิ่มความสามารถได้อย่างปลอดภัยและทรงพลัง ผ่าน idiom ของ wrapper class ถ้าคุณใช้คลาส abstract นิยามชนิด โปรแกรมเมอร์ที่ต้องการเพิ่ม ความสามารถจะไม่มีทางเลือกอื่นนอกจาก inheritance
15. Generics: อย่าใช้ raw type
คุณจะไม่พบข้อผิดพลาดจนกว่าจะถึงตอนรันไทม์ ซึ่งนานหลังจากที่มันเกิดขึ้น และในโค้ดที่อาจอยู่ห่างไกลจากโค้ดที่มีข้อผิดพลาด เมื่อเจอ ClassCastException คุณก็ต้องค้นหา ไปทั่วโค้ดเบสเพื่อหาการเรียกเมธอดที่ใส่เหรียญลงไปในคอลเลกชันแสตมป์ คอมไพเลอร์ช่วยอะไรคุณไม่ได้
16. Generics: กำจัดคำเตือน unchecked
เมื่อเขียนโปรแกรมด้วย generics คุณจะเห็นคำเตือนจากคอมไพเลอร์มากมาย:
- คำเตือน unchecked cast
- คำเตือน unchecked method invocation
- คำเตือน unchecked parameterized vararg type
- คำเตือน unchecked conversion
ยิ่งคุณมีประสบการณ์กับ generics มากเท่าไร คำเตือนก็จะน้อยลงเท่านั้น แต่อย่าคาดหวังว่าโค้ดที่เพิ่งเขียนจะคอมไพล์ผ่านโดยไม่มีคำเตือนเลย คำเตือน unchecked หลายอย่างกำจัดได้ง่าย ตัวอย่างเช่น สมมติว่าคุณเผลอเขียนการประกาศแบบนี้:
Set<Lark> exaltation = new HashSet();
ให้เปลี่ยนเป็น:
Set<Lark> exaltation = new HashSet<>();
คำเตือนบางอย่างกำจัดได้ยากกว่ามาก เมื่อเจอคำเตือนที่ต้องใช้ความคิด ก็อย่าย่อท้อ! กำจัดคำเตือน unchecked ทุกตัวที่ทำได้
ถ้าคุณกำจัดคำเตือนได้ทั้งหมด ก็มั่นใจได้ว่าโค้ดของคุณ typesafe ซึ่งเป็นเรื่องดีมาก หมายความว่าคุณจะไม่เจอ ClassCastException ตอนรันไทม์ และยังเพิ่มความมั่นใจ
ว่าโปรแกรมจะทำงานตามที่คุณตั้งใจ หากคุณกำจัดคำเตือนไม่ได้ แต่พิสูจน์ได้ว่าโค้ดที่ทำให้เกิดคำเตือนนั้น typesafe เมื่อนั้น (และเมื่อนั้นเท่านั้น) จึงค่อยระงับ
คำเตือนด้วย annotation @SuppressWarnings("unchecked") หากคุณระงับคำเตือนโดยไม่พิสูจน์ก่อนว่าโค้ด typesafe ก็เท่ากับคุณกำลังหลอกตัวเอง
ว่าปลอดภัย
โค้ดอาจคอมไพล์ผ่านโดยไม่มีคำเตือนใด ๆ แต่ก็ยังโยน ClassCastException ตอนรันไทม์ได้
ทุกครั้งที่ใช้ annotation @SuppressWarnings("unchecked") ให้เขียนคอมเมนต์อธิบายว่าทำไมจึงปลอดภัย
17. Generics: เมื่อไรที่สมควรใช้
โอ๊ย – คลาสที่ต้องการ generics อย่างยิ่ง:
อะไรแบบนี้ควรถูกจับได้อย่างช้าที่สุดตอนทำ merge request!
18. Generics: ตัวอย่างที่ไม่ควรทำ
ขอโทษนะ ไม่ต้องพูดอะไรมาก ได้โปรดอย่า merge โค้ดแบบนี้เด็ดขาด:
public abstract class JpaLearningCardBaseContentService<LC extends LearningCardBaseContent<L, LC, AC>, J extends JpaLearningCardBaseContent<L, LC, AC>, L extends
LearningCardBase<LC, A>, A extends AnswerOptionBase<L, AC>, AC extends AnswerOptionBaseContent<LC, AC, A>, AS extends AnswerOptionBaseService<A, AC, L, LC>, ACS extends
AnswerOptionBaseContentService<AC, A, L, LC>, LS extends LearningCardBaseService<L>>
extends JpaLightPersistenceService<LC, J, Long> implements LearningCardBaseContentService<LC, L, A, AC> {
// ...
}
แค่การประกาศคลาส ไม่ควรต้องใช้เวลา 5 นาทีกว่าจะเข้าใจ
19. เลือกใช้ list มากกว่าอาร์เรย์
อาร์เรย์ต่างจากชนิด generic ในสองประเด็นสำคัญ ประการแรก อาร์เรย์เป็น covariant คำที่ฟังดูน่ากลัวนี้หมายความง่าย ๆ ว่า ถ้า Sub เป็น subtype ของ Super
ชนิดอาร์เรย์ Sub[] ก็เป็น subtype ของชนิดอาร์เรย์ Super[] ในทางตรงกันข้าม generics เป็น invariant: สำหรับสองชนิดใด ๆ ที่ต่างกัน Type1 และ Type2
List<Type1> ไม่ได้เป็นทั้ง subtype และ supertype ของ List<Type2>
20. เลือกใช้ lambda มากกว่า anonymous class
// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
ให้เขียนแบบนี้แทน:
// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
21. จำกัดขอบเขตของตัวแปรภายในให้แคบที่สุด
เทคนิคที่ทรงพลังที่สุดในการจำกัดขอบเขตของตัวแปรภายในคือประกาศมันตรงจุดที่ใช้งานครั้งแรก...
22. เลือกใช้ลูป for-each มากกว่าลูป for แบบดั้งเดิม
// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
... // Do something with e
}
23. รู้จักและใช้ไลบรารี
สมมติว่าคุณต้องการสุ่มจำนวนเต็มระหว่างศูนย์ถึงค่าขอบบนค่าหนึ่ง เมื่อเจองานธรรมดาแบบนี้ โปรแกรมเมอร์หลายคนจะเขียนเมธอดเล็ก ๆ หน้าตาประมาณนี้:
// Common but deeply flawed!
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
เมธอดนี้อาจดูดี แต่มีข้อบกพร่องสามข้อ ข้อแรก ถ้า n เป็นเลขยกกำลังของสองที่มีค่าน้อย ลำดับของตัวเลขสุ่มจะวนซ้ำหลังจากผ่านไปช่วงสั้น ๆ ข้อบกพร่องที่สองคือ ถ้า n ไม่ใช่เลขยกกำลังของสอง ตัวเลขบางตัวจะถูกคืนค่าบ่อยกว่าตัวอื่นโดยเฉลี่ย ถ้า n มีค่ามาก ผลนี้จะเห็นได้ชัดมาก
ข้อบกพร่องที่สามของเมธอด random คือในบางโอกาสที่เกิดขึ้นได้ยาก มันอาจล้มเหลวอย่างร้ายแรง โดยคืนค่าตัวเลขที่อยู่นอกช่วงที่กำหนด สาเหตุคือ
เมธอดพยายามแปลงค่าที่ได้จาก rnd.nextInt() ให้เป็น int ที่ไม่ติดลบด้วยการเรียก Math.abs ถ้า nextInt() คืนค่า
Integer.MIN_VALUE แล้ว Math.abs ก็จะคืนค่า Integer.MIN_VALUE ด้วย และตัวดำเนินการหาเศษ (%) จะคืนค่าติดลบ โดยสมมติว่า
n ไม่ใช่เลขยกกำลังของสอง สิ่งนี้แทบจะทำให้โปรแกรมของคุณล้มเหลวแน่นอน และความล้มเหลวนั้นอาจทำให้เกิดซ้ำได้ยาก
24. หลีกเลี่ยง float และ double ถ้าต้องการคำตอบที่แม่นยำ
ใช้ BigDecimal สำหรับจำนวนเงิน float และ double ทำให้เกิดความคลาดเคลื่อนจากการปัดเศษ
ตัวอย่างเช่น สมมติว่าคุณมีเงิน $1.03 ในกระเป๋า และใช้ไป 42¢ คุณจะเหลือเงินเท่าไร? นี่คือโค้ดส่วนหนึ่งแบบซื่อ ๆ ที่พยายามตอบคำถามนี้:
System.out.println(1.03 - 0.42);
น่าเสียดายที่มันพิมพ์ออกมาเป็น 0.6100000000000001 และนี่ไม่ใช่กรณีเดียว สมมติว่าคุณมีเงินหนึ่งดอลลาร์ในกระเป๋า แล้วซื้อแหวนรองเกลียวเก้าตัว ราคาตัวละสิบเซนต์ คุณจะได้
เงินทอนเท่าไร?
System.out.println(1.00 - 9 * 0.10);
ตามโค้ดส่วนนี้ คุณจะได้เงินทอน $0.09999999999999998
คุณอาจคิดว่าแก้ปัญหานี้ได้แค่ปัดเศษผลลัพธ์ก่อนพิมพ์ แต่น่าเสียดายที่วิธีนี้ใช้ไม่ได้เสมอไป ตัวอย่างเช่น สมมติว่าคุณมีเงินหนึ่งดอลลาร์ในกระเป๋า และเห็นชั้นวางที่มีลูกอมแสนอร่อยเรียงเป็นแถว ราคา 10¢, 20¢, 30¢ ไปเรื่อย ๆ จนถึงหนึ่งดอลลาร์ คุณซื้อลูกอมทีละชิ้น เริ่มจากชิ้นที่ราคา 10¢ จนกว่าเงินจะไม่พอ ซื้อชิ้นถัดไปบนชั้น
25. เลือกใช้ชนิด primitive มากกว่า boxed primitive
// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
26. อ้างถึงอ็อบเจกต์ผ่านอินเทอร์เฟซของมัน
ตัวอย่างที่ดี:
Set<Son> sonSet = new LinkedHashSet<>();
ตัวอย่างที่ไม่ดี:
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
27. เลือกใช้อินเทอร์เฟซมากกว่า reflection
ถ้าไม่ทำเช่นนั้น:
- คุณจะเสียประโยชน์ทั้งหมดของการตรวจสอบชนิดตอนคอมไพล์ รวมถึงการตรวจสอบ exception ถ้าโปรแกรมพยายามเรียกเมธอดที่ไม่มีอยู่หรือเข้าถึงไม่ได้ผ่าน reflection มันจะล้มเหลวตอนรันไทม์ เว้นแต่คุณจะเตรียมการป้องกันไว้เป็นพิเศษ
- โค้ดที่ต้องใช้เพื่อเข้าถึงแบบ reflection นั้นเทอะทะและยืดยาว เขียนน่าเบื่อและอ่านยาก
- ประสิทธิภาพลดลง การเรียกเมธอดผ่าน reflection ช้ากว่าการเรียกเมธอดปกติมาก
28. ยึดตามหลักการตั้งชื่อที่เป็นที่ยอมรับทั่วไป
แพลตฟอร์ม Java มีหลักการตั้งชื่อที่เป็นที่ยอมรับอย่างแพร่หลาย ซึ่งหลายข้ออยู่ใน The Java Language Specification [JLS, 6.1]
29. ใช้ exception เฉพาะกับสถานการณ์ที่ผิดปกติเท่านั้น
// Horrible abuse of exceptions. Don't ever do this!
try {
int i = 0;
while (true) {
range[i++].climb();
}
} catch (ArrayIndexOutOfBoundsException e) {
// catch it
}
โค้ดนี้ทำอะไร? ดูแล้วไม่ชัดเจนเลย และนั่นก็เป็นเหตุผลเพียงพอที่จะไม่ใช้มัน ปรากฏว่ามันเป็น idiom ที่คิดมาอย่างผิดพลาดร้ายแรงสำหรับการวนลูปผ่านสมาชิกของ
อาร์เรย์ ลูปไม่รู้จบนี้จะจบลงด้วยการโยน จับ และเพิกเฉยต่อ ArrayIndexOutOfBoundsException เมื่อมัน
พยายามเข้าถึงสมาชิกตัวแรกที่อยู่นอกขอบเขตของอาร์เรย์ มันควรจะเทียบเท่ากับ idiom มาตรฐานสำหรับวนลูปผ่านอาร์เรย์ ซึ่งโปรแกรมเมอร์ Java ทุกคน
จำได้ทันที:
for (Mountain m : range) {
m.climb();
}
exception มีไว้ใช้เฉพาะกับสถานการณ์ที่ผิดปกติ (exceptional) ตามชื่อของมันเท่านั้น และไม่ควรใช้ควบคุมลำดับการทำงานตามปกติเลย
30. ใช้ checked exception กับสถานการณ์ที่กู้คืนได้ และใช้ runtime exception กับข้อผิดพลาดจากการเขียนโปรแกรม
ใช้ checked exception กับสถานการณ์ที่คาดหวังได้อย่างสมเหตุสมผลว่าผู้เรียกจะกู้คืนได้ การบังคับให้ผู้ใช้ต้องรับมือกับ checked exception คือการที่ผู้ออกแบบ API กำหนดให้ต้องกู้คืนจากสถานการณ์นั้น ผู้ใช้อาจเพิกเฉยต่อข้อกำหนดนี้ได้ด้วยการจับ exception แล้วไม่สนใจ แต่โดยทั่วไปนั่นเป็นความคิดที่ไม่ดี
ใช้ runtime exception เพื่อบ่งบอกข้อผิดพลาดจากการเขียนโปรแกรม runtime exception ส่วนใหญ่บ่งบอกว่ามีการละเมิดเงื่อนไขเบื้องต้น (precondition) การละเมิดเงื่อนไขเบื้องต้นก็คือการที่
client ของ API ไม่ปฏิบัติตามสัญญาที่กำหนดไว้ในข้อกำหนดของ API ตัวอย่างเช่น สัญญาของการเข้าถึงอาร์เรย์กำหนดว่าดัชนีต้องอยู่ระหว่างศูนย์ถึงความยาวของ
อาร์เรย์ลบหนึ่ง รวมทั้งสองค่า ArrayIndexOutOfBoundsException บ่งบอกว่ามีการละเมิดเงื่อนไขเบื้องต้นนี้
31. หลีกเลี่ยงการใช้ checked exception โดยไม่จำเป็น
โดยสรุป เมื่อใช้อย่างพอประมาณ checked exception ช่วยเพิ่มความน่าเชื่อถือของโปรแกรม แต่เมื่อใช้มากเกินไป ก็ทำให้ API ใช้งานลำบาก
Pair<Boolean, LearningCardGroup> answerLearningCard(@NotNull LearningCardGroup learningCardGroup,
LearningCardAssignment learningCardAssignment,
@NotNull Collection<AnswerOption> answers)
throws InvalidTrainingStateException, PersistenceException, LearningCardGroupAlreadyCompletedException,
ConsecutiveCorrectCountExceededException,
PreviousLearningCardGroupUncompletedException, LearningCardGroupCooldownException,
LearningCardGroupHasUncompletedLearningCardsException, InvalidParticipantException, TrainingNotSignedException; // but why ???
// this is a mistake seen a lot with junior devs
// exceptions are exceptionally slow and should be avoided for mere validation
// the performance gets even worse if a stacktrace is printed, since logging and printing is one of the slowest possible actions
32. เลือกใช้ exception มาตรฐาน
คุณสมบัติที่แยกโปรแกรมเมอร์ผู้เชี่ยวชาญออกจากผู้ที่มีประสบการณ์น้อยกว่า คือผู้เชี่ยวชาญมุ่งมั่นและมักทำได้สำเร็จในการนำโค้ดกลับมาใช้ใหม่ในระดับสูง
exception ก็ไม่ได้เป็นข้อยกเว้นของกฎที่ว่าการนำโค้ดกลับมาใช้ใหม่เป็นสิ่งดี ไลบรารีของ Java มีชุด exception ที่ครอบคลุมความต้องการในการโยน exception ของ API ส่วนใหญ่ การนำ exception มาตรฐานกลับมาใช้มีประโยชน์หลายอย่าง ข้อสำคัญที่สุดคือทำให้ API ของคุณเรียนรู้และใช้งานได้ง่ายขึ้น เพราะสอดคล้องกับธรรมเนียมที่โปรแกรมเมอร์คุ้นเคยอยู่แล้ว รองลงมาไม่ห่างกันคือ โปรแกรมที่ใช้ API ของคุณจะอ่านง่ายขึ้น เพราะไม่รกไปด้วย exception ที่ไม่คุ้นเคย ข้อสุดท้าย (และสำคัญน้อยที่สุด) คือ คลาส exception ที่น้อยลงหมายถึงการใช้หน่วยความจำ ที่น้อยลง และใช้เวลาโหลดคลาสน้อยลง
ชนิด exception ที่ถูกนำกลับมาใช้บ่อยที่สุดคือ IllegalArgumentException โดยทั่วไปนี่คือ exception ที่ควรโยนเมื่อผู้เรียกส่งอาร์กิวเมนต์ที่มีค่าไม่เหมาะสมเข้ามา ตัวอย่างเช่น
ควรโยน exception นี้ถ้าผู้เรียกส่งตัวเลขติดลบเข้ามาในพารามิเตอร์ที่แทนจำนวนครั้งที่ต้องทำบางอย่างซ้ำ exception อีกตัวที่ถูกนำกลับมาใช้บ่อยคือ
IllegalStateException โดยทั่วไปนี่คือ exception ที่ควรโยนเมื่อการเรียกนั้นไม่ถูกต้องเพราะ state ของอ็อบเจกต์ที่ถูกเรียก ตัวอย่างเช่น ควรโยน
exception นี้ถ้าผู้เรียกพยายามใช้อ็อบเจกต์ก่อนที่จะถูกกำหนดค่าเริ่มต้นอย่างถูกต้อง
อาจกล่าวได้ว่า การเรียกเมธอดที่ผิดพลาดทุกครั้งล้วนมาจากอาร์กิวเมนต์หรือ state ที่ไม่ถูกต้อง แต่ก็มี exception อื่นที่ใช้เป็นมาตรฐานกับอาร์กิวเมนต์และ state ที่ไม่ถูกต้องบางประเภท ถ้าผู้เรียก
ส่ง null เข้ามาในพารามิเตอร์ที่ห้ามค่า null ตามธรรมเนียมควรโยน NullPointerException แทน IllegalArgumentException
ในทำนองเดียวกัน ถ้าผู้เรียกส่งค่าที่อยู่นอกช่วงเข้ามาในพารามิเตอร์ที่แทนดัชนีของลำดับ ควรโยน IndexOutOfBoundsException แทน IllegalArgumentException
อย่านำ Exception, RuntimeException, Throwable หรือ Error มาใช้โดยตรง ให้ถือว่าคลาสเหล่านี้เป็นคลาส abstract อย่างไรก็ตาม อาจใช้ RuntimeException ได้ถ้าคุณต้องการ
แปลง checked exception ให้เป็น runtime exception และรู้จริง ๆ ว่ากำลังทำอะไรอยู่
33. อย่าเพิกเฉยต่อ exception
// Empty catch block ignores exception - Highly suspect!
try {
...
} catch (SomeException e) {
}
บล็อก catch ที่ว่างเปล่าทำลายจุดประสงค์ของ exception ซึ่งก็คือการบังคับให้คุณจัดการกับสถานการณ์ที่ผิดปกติ การเพิกเฉยต่อ exception ก็เหมือนการเพิกเฉยต่อสัญญาณเตือนไฟไหม้ – แล้ว ปิดมันเสียด้วย จนไม่มีใครได้มีโอกาสดูว่ามีไฟไหม้จริงหรือเปล่า