นี่คือโพสต์ที่แปลด้วย AI
[Effective Java] รายการ 6. หลีกเลี่ยงการสร้างอ็อบเจ็กต์ที่ไม่จำเป็น
- ภาษาที่เขียน: ภาษาเกาหลี
- •
- ประเทศอ้างอิง: ทุกประเทศ
- •
- เทคโนโลยีสารสนเทศ
เลือกภาษา
สรุปโดย AI ของ durumis
- การสร้างอินสแตนซ์ String หรือ Boolean โดยใช้คีย์เวิร์ด new เป็นการสิ้นเปลืองหน่วยความจำ ดังนั้นจึงควรประกาศเป็นลิเทอรัล หรือใช้เมธอด Boolean.valueOf()
- เมธอด String.matches() ใช้การแสดงออกทั่วไป ดังนั้นจึงอาจทำให้ประสิทธิภาพลดลง ควรแคชอินสแตนซ์ Pattern เพื่อนำกลับมาใช้ใหม่
- ในกรณีที่คืนค่าอ็อบเจ็กต์แบบมุมมอง เช่น เมธอด keySet() การใช้การคัดลอกแบบป้องกันเพื่อคืนค่าอ็อบเจ็กต์ใหม่จะปลอดภัยกว่า
กรณีการสร้างอ็อบเจกต์ที่ไม่จำเป็น
การใช้ new String()
String a = new String("hi");
String b = new String("hi");
สตริง a, b, c ทั้งหมดจะมีสตริง "hi" แต่เนื่องจากที่อยู่ที่สตริงทั้งสามนี้อ้างอิงถึงนั้นต่างกัน จึงเกิดการสิ้นเปลืองที่ต้องจัดสรรหน่วยความจำที่แตกต่างกันสำหรับข้อมูลเดียวกัน
ดังนั้น เมื่อประกาศสตริง เราไม่ควรใช้คีย์เวิร์ด new แต่ควรประกาศเป็นลิเทอรัล
String a = "hi";
String b = "hi";
โค้ดข้างต้นใช้เพียงอินสแตนซ์เดียว นอกจากนี้ การใช้วิธีนี้ยังรับประกันว่าทุกโค้ดที่ใช้ลิเทอรัลสตริง "hi" ภายใน JVM เดียวกัน จะใช้オブジェクトเดียวกัน การใช้ลิเทอรัลสตริงนี้เกิดจากลักษณะเฉพาะของพูลค่าคงที่ของ Java
การใช้ new Boolean()
โค้ดข้างต้นสร้างอินสแตนซ์ Boolean ผ่านคอนสตรัคเตอร์ที่รับสตริงเป็นพารามิเตอร์ Boolean มีเพียง true หรือ false เท่านั้น การสร้างอินสแตนซ์ในแต่ละครั้งเป็นการสิ้นเปลืองหน่วยความจำ ดังนั้น การใช้เมธอดแบบคงที่คือ Boolean.valueOf() จะดีกว่า
การใช้ String.matches()
ถ้าค่าใช้จ่ายในการสร้างมีมาก การแคชเพื่อนำกลับมาใช้ใหม่เป็นสิ่งที่ดี แต่เราไม่สามารถทราบค่าใช้จ่ายของอ็อบเจกต์ที่เราสร้างได้ตลอดเวลา ตัวอย่างเช่น สมมติว่าเราต้องการเขียนเมธอดที่ตรวจสอบว่าสตริงที่กำหนดเป็นเลขโรมันที่ถูกต้องหรือไม่ เราสามารถใช้การแสดงออกปกติ ตามที่แสดงด้านล่างได้ง่ายที่สุด
public static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
แต่ String.matches() เป็นเมธอดที่มีปัญหาในแง่ของประสิทธิภาพ อินสแตนซ์ Pattern สำหรับการแสดงออกปกติที่เมธอดนี้สร้างขึ้นภายใน จะถูกใช้เพียงครั้งเดียวแล้วทิ้งไป ทำให้กลายเป็นเป้าหมายของการรวบรวมขยะอย่างรวดเร็ว แต่ถ้าการแสดงออกปกติใช้ซ้ำบ่อย ค่าใช้จ่ายในการสร้างและทิ้งอินสแตนซ์ Pattern เดียวกันจะเพิ่มขึ้น ดังนั้น ควรแคชอินสแตนซ์ Pattern ไว้ล่วงหน้า และนำกลับมาใช้ใหม่ทุกครั้งที่เรียกใช้เมธอด isRomanNumeral()
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile(
"^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
public static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
หมายเหตุ
ในตัวอย่างทั้งหมดข้างต้น เมื่อแคชอ็อบเจกต์ที่ไม่จำเป็น เราสร้างเป็นอ็อบเจกต์แบบไม่แปรเปลี่ยน เพราะการนำกลับมาใช้ใหม่ จะปลอดภัย แต่บางกรณีก็ขัดกับสัญชาตญาณในการนำกลับมาใช้ใหม่โดยใช้อ็อบเจกต์แบบไม่แปรเปลี่ยน
อะแดปเตอร์ (วิว) เป็นอ็อบเจกต์ที่มอบหมายงานจริงให้กับอ็อบเจกต์หลังและทำหน้าที่เป็นอินเทอร์เฟซที่สอง อะแดปเตอร์ ต้องจัดการกับอ็อบเจกต์หลังเท่านั้น ดังนั้น ควรสร้างอะแดปเตอร์หนึ่งตัวต่ออ็อบเจกต์หลังหนึ่งตัว
ตัวอย่างเช่น เมธอด keySet() ของอินเทอร์เฟซ Map ส่งคืนวิว Set ที่มีทุกคีย์ภายในอ็อบเจกต์ Map ผู้ใช้ อาจคิดว่าจะสร้างอินสแตนซ์ Set ใหม่ทุกครั้งที่เรียกใช้เมธอด keySet() แต่ในความเป็นจริง การใช้งาน JDK แสดงให้เห็นว่า จะส่งคืนอินสแตนซ์ Set แบบเปลี่ยนแปลงได้เดียวกันทุกครั้ง
สาเหตุนี้เนื่องจากแม้ว่าอินสแตนซ์ Set ที่ส่งคืนจะเป็นแบบเปลี่ยนแปลงได้ แต่ฟังก์ชันที่ทำจะเหมือนกันทั้งหมด และอินสแตนซ์ Set ทั้งหมดจะเป็นตัวแทนของอินสแตนซ์ Map ดังนั้น แม้ว่า keySet() จะสร้างวิวอ็อบเจกต์ หลายตัว ก็ไม่มีผลกระทบใดๆ แต่ก็ไม่มีความจำเป็นและไม่เกิดประโยชน์เช่นกัน
public class UsingKeySet {
public static void main(String[] args) {
Map menu = new HashMap<>();
menu.put("Burger", 8);
menu.put("Pizza", 9);
Set names1 = menu.keySet();
Set names2 = menu.keySet();
names1.remove("Burger");
System.out.println(names1.size()); // 1
System.out.println(names2.size()); // 1
}
ดังนั้น การแก้ไขอินสแตนซ์ names1 จะส่งผลกระทบต่ออินสแตนซ์ names2 เช่นกัน
อย่างไรก็ตาม ในความคิดส่วนตัว ฉันคิดว่าค่าที่ส่งคืนโดยเมธอด keySet() ควรใช้การคัดลอกแบบป้องกัน เพื่อส่งคืนอ็อบเจกต์ใหม่ทุกครั้ง ถ้าอินสแตนซ์ Set ที่รับจากเมธอด keySet() ถูกใช้ในที่อื่น และมีโค้ดที่เปลี่ยนแปลงสถานะของอินสแตนซ์นี้ เราจะไม่สามารถมั่นใจได้ในค่าของอินสแตนซ์ Set และอินสแตนซ์ Map ที่ใช้อยู่
นอกจากนี้ เว้นแต่จะใช้ keySet() บ่อยมาก การสร้างอินเทอร์เฟซ Set ในแต่ละครั้งจะไม่ส่งผลกระทบอย่างร้ายแรงต่อ ประสิทธิภาพ ฉันคิดว่าการสร้างอินเทอร์เฟซ Set เป็นอ็อบเจกต์แบบไม่แปรเปลี่ยนเพื่อให้สามารถบำรุงรักษาได้อย่างมั่นคง จะดีกว่า
การบรรจุอัตโนมัติ
การบรรจุอัตโนมัติเป็นเทคโนโลยีที่ช่วยให้โปรแกรมเมอร์สามารถผสมผสานประเภทพื้นฐาน และประเภท wrapper ได้โดยการแปลงแบบอัตโนมัติ แต่การบรรจุอัตโนมัติไม่ใช่การลบ ความแตกต่างระหว่างประเภทพื้นฐานและประเภท wrapper ออกไปทั้งหมด
public static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
return sum;
ในเชิงตรรกะ โค้ดนี้ไม่มีปัญหา แต่ในแง่ของประสิทธิภาพ โค้ดนี้ไม่มีประสิทธิภาพอย่างมาก สาเหตุ เกิดจากประเภทของ sum และประเภทของ i ภายในลูป for
ประเภทของ sum คือ Long และประเภทของ i คือ long ดังนั้น i ซึ่งเป็นประเภท long จะถูกสร้างเป็นอินสแตนซ์ Long ใหม่ทุกครั้งเมื่อเพิ่มเข้าไปใน sum ขณะที่ลูปทำงาน ผลลัพธ์คือ ควรใช้ประเภทพื้นฐานมากกว่าประเภท wrapper และควรระมัดระวัง เพื่อไม่ให้การบรรจุอัตโนมัติที่ไม่ต้องการถูกใช้
สิ่งที่ไม่ควรเข้าใจผิด
ไม่ควรเข้าใจผิดว่าการหลีกเลี่ยงการสร้างอ็อบเจกต์ที่ไม่จำเป็นนั้นเป็นเพียงการหลีกเลี่ยง การสร้างอ็อบเจกต์เพราะค่าใช้จ่ายในการสร้างมีมาก
โดยเฉพาะอย่างยิ่ง ในปัจจุบัน JVM ไม่ได้รู้สึกหนักใจกับการสร้างและรวบรวมอ็อบเจกต์ขนาดเล็ก ที่สร้างขึ้นโดยไม่จำเป็น ดังนั้น หากไม่ใช่อ็อบเจกต์ที่มีค่าใช้จ่ายสูง เช่น การเชื่อมต่อฐานข้อมูล ควรหลีกเลี่ยงการสร้างพูลอ็อบเจกต์แบบกำหนดเอง
นอกจากนี้ ควรจำไว้ว่าความเสียหายที่เกิดขึ้นจากการนำกลับมาใช้ใหม่ในสถานการณ์ที่จำเป็น ต้องใช้การคัดลอกแบบป้องกันมีมากกว่าความเสียหายที่เกิดจากการสร้างอ็อบเจกต์ที่ไม่ต้องการ ซ้ำๆ ความเสียหายที่เกิดจากการสร้างซ้ำๆ จะส่งผลต่อรูปแบบโค้ดและประสิทธิภาพเท่านั้น แต่การล้มเหลวของการคัดลอกแบบป้องกันจะนำไปสู่บั๊กและปัญหาความปลอดภัย