5 ข้อผิดพลาดที่ 99% ของ Java Developer ทำ
ที่มา:
สื่อ ในโพสต์นี้ คุณจะได้เรียนรู้เกี่ยวกับข้อผิดพลาดทั่วไปที่ Java Developer จำนวนมากทำ ในฐานะโปรแกรมเมอร์ Java ฉันรู้ว่าการใช้เวลามากมายไปกับการแก้ไขจุดบกพร่องในโค้ดของคุณนั้นแย่แค่ไหน บางครั้งอาจใช้เวลาหลายชั่วโมง อย่างไรก็ตาม มีข้อผิดพลาดมากมายเกิดขึ้นเนื่องจากการที่นักพัฒนาละเลยกฎพื้นฐาน นั่นคือข้อผิดพลาดระดับต่ำมาก วันนี้เราจะมาดูข้อผิดพลาดทั่วไปในการเขียนโค้ดแล้วอธิบายวิธีแก้ไข ฉันหวังว่านี่จะช่วยคุณหลีกเลี่ยงปัญหาในการทำงานประจำวันของคุณได้
การเปรียบเทียบวัตถุโดยใช้ Objects.equals
ฉันคิดว่าคุณคุ้นเคยกับวิธีนี้ นักพัฒนาหลายคนใช้มันบ่อยๆ เทคนิคนี้ที่นำมาใช้ใน JDK 7 ช่วยให้คุณเปรียบเทียบวัตถุได้อย่างรวดเร็ว และหลีกเลี่ยงการตรวจสอบตัวชี้ว่างที่น่ารำคาญได้อย่างมีประสิทธิภาพ แต่บางครั้งวิธีนี้ก็ใช้ไม่ถูกต้อง นี่คือสิ่งที่ฉันหมายถึง:
Long longValue = 123L;
System.out.println(longValue==123);
System.out.println(Objects.equals(longValue,123));
เหตุใดการแทนที่
==ด้วย
Objects.equals()จึงให้ผลลัพธ์ที่ผิด เนื่องจาก คอมไพลเลอร์
==จะได้รับประเภทข้อมูลพื้นฐานที่สอดคล้องกับ ประเภทบรรจุภัณฑ์
longValueแล้วเปรียบเทียบกับประเภทข้อมูลพื้นฐานนั้น ซึ่งเทียบเท่ากับคอมไพลเลอร์ที่แปลงค่าคงที่เป็นประเภทข้อมูลการเปรียบเทียบพื้นฐานโดยอัตโนมัติ หลังจากใช้ เมธอด
Objects.equals()ชนิดข้อมูลพื้นฐานเริ่มต้นของค่าคงที่คอมไพลเลอร์จะเป็น
int ด้านล่างนี้คือซอร์สโค้ด
สำหรับ Objects.equals()โดยที่
a.equals(b)ใช้
Long.equals()และกำหนดประเภทของออบเจ็กต์ สิ่งนี้เกิดขึ้นเนื่องจากคอมไพลเลอร์สันนิษฐานว่าค่าคงที่นั้นเป็นประเภท
intดังนั้นผลลัพธ์ของการเปรียบเทียบจะต้องเป็นเท็จ
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
public boolean equals(Object obj) {
if (obj instanceof Long) {
return value == ((Long)obj).longValue();
}
return false;
}
การรู้เหตุผลการแก้ไขข้อผิดพลาดนั้นง่ายมาก เพียงประกาศประเภทข้อมูลของค่าคงที่เช่นObjects.equals
(longValue,123L) ปัญหาข้างต้นจะไม่เกิดขึ้นหากตรรกะเข้มงวด สิ่งที่เราต้องทำคือปฏิบัติตามกฎการเขียนโปรแกรมที่ชัดเจน
รูปแบบวันที่ไม่ถูกต้อง
ในการพัฒนาในแต่ละวันคุณมักจะต้องเปลี่ยนวันที่ แต่หลายคนใช้รูปแบบที่ไม่ถูกต้องซึ่งนำไปสู่สิ่งที่ไม่คาดคิด นี่คือตัวอย่าง:
Instant instant = Instant.parse("2021-12-31T00:00:00.00Z");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault());
System.out.println(formatter.format(instant));
ซึ่งใช้ รูปแบบ
YYYY-MM-ddเพื่อเปลี่ยนวันที่จาก 2021 เป็น 2022 คุณไม่ควรทำอย่างนั้น ทำไม เนื่องจาก รูปแบบ
Java DateTimeFormatter “YYYY” เป็นไปตามมาตรฐาน ISO-8601 ซึ่งกำหนดปีเป็นวันพฤหัสบดีของแต่ละสัปดาห์ แต่วันที่ 31 ธันวาคม 2564 ตรงกับวันศุกร์ โปรแกรมจึงระบุปี 2565 ไม่ถูกต้อง เพื่อหลีกเลี่ยงปัญหานี้ คุณต้องใช้รูปแบบ
yyyy-MM-dd เพื่อจัดรูปแบบวัน ที่ ข้อผิดพลาดนี้เกิดขึ้นไม่บ่อยนัก เฉพาะเมื่อถึงปีใหม่เท่านั้น แต่ในบริษัทของฉัน มันทำให้การผลิตล้มเหลว
การใช้ ThreadLocal ใน ThreadPool
หากคุณสร้าง
ตัวแปร ThreadLocalเธรดที่เข้าถึงตัวแปรนั้นจะสร้างตัวแปรโลคัลเธรด วิธีนี้ทำให้คุณสามารถหลีกเลี่ยงปัญหาความปลอดภัยของเธรดได้ อย่างไรก็ตาม หากคุณใช้
ThreadLocalบน
thread poolคุณจะต้องระมัดระวัง รหัสของคุณอาจให้ผลลัพธ์ที่ไม่คาดคิด สำหรับตัวอย่างง่ายๆ สมมติว่าเรามีแพลตฟอร์มอีคอมเมิร์ซและผู้ใช้จำเป็นต้องส่งอีเมลเพื่อยืนยันการซื้อผลิตภัณฑ์ที่เสร็จสมบูรณ์
private ThreadLocal<User> currentUser = ThreadLocal.withInitial(() -> null);
private ExecutorService executorService = Executors.newFixedThreadPool(4);
public void executor() {
executorService.submit(()->{
User user = currentUser.get();
Integer userId = user.getId();
sendEmail(userId);
});
}
หากเราใช้
ThreadLocalเพื่อบันทึกข้อมูลผู้ใช้ ข้อผิดพลาดที่ซ่อนอยู่จะปรากฏขึ้น เนื่องจากมีการใช้พูลของเธรด และสามารถนำเธรดกลับมาใช้ใหม่ได้ เมื่อใช้
ThreadLocalเพื่อรับข้อมูลผู้ใช้ ก็อาจแสดงข้อมูลของบุคคลอื่นอย่างผิดพลาดได้ เพื่อแก้ไขปัญหานี้ คุณควรใช้เซสชัน
ใช้ HashSet เพื่อลบข้อมูลที่ซ้ำกัน
เมื่อเขียนโค้ด เรามักจะจำเป็นต้องขจัดข้อมูลซ้ำซ้อน เมื่อคุณนึกถึงการขจัดข้อมูลซ้ำซ้อน สิ่งแรกที่หลายคนนึกถึงคือการใช้
HashSet อย่างไรก็ตาม การใช้
HashSet โดยไม่ระมัดระวัง อาจทำให้การขจัดข้อมูลซ้ำซ้อนล้มเหลวได้
User user1 = new User();
user1.setUsername("test");
User user2 = new User();
user2.setUsername("test");
List<User> users = Arrays.asList(user1, user2);
HashSet<User> sets = new HashSet<>(users);
System.out.println(sets.size());
ผู้อ่านที่เอาใจใส่บางคนควรจะสามารถเดาสาเหตุของความล้มเหลวได้
HashSetใช้รหัสแฮชเพื่อเข้าถึงตารางแฮชและใช้วิธีการเท่ากับเพื่อกำหนดว่าวัตถุเท่ากันหรือไม่ หากวัตถุที่ผู้ใช้กำหนดไม่ได้แทนที่วิธี hashcode และ วิธี
เท่ากับ วิธี การดังกล่าวจะใช้วิธี hashcode และ วิธี
เท่ากับ ของวัตถุหลักเป็นค่าเริ่มต้น ซึ่งจะ ทำให้
HashSetสันนิษฐานว่าเป็นวัตถุสองชิ้นที่แตกต่างกัน ส่งผลให้การขจัดข้อมูลซ้ำซ้อนล้มเหลว
กำจัดด้ายสระน้ำที่ "กิน"
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(()->{
double result = 10/0;
});
โค้ดด้านบนจำลองสถานการณ์ที่มีข้อยกเว้นเกิดขึ้นในเธรดพูล รหัสธุรกิจต้องยอมรับสถานการณ์ต่างๆ ดังนั้นจึงมีโอกาสมากที่จะส่ง
RuntimeException ด้วยเหตุผลบาง ประการ แต่หากไม่มีการจัดการพิเศษที่นี่ ข้อยกเว้นนี้จะถูก "กิน" โดยเธรดพูล และคุณจะไม่มีวิธีตรวจสอบสาเหตุของข้อยกเว้นด้วยซ้ำ ดังนั้นจึงเป็นการดีที่สุดที่จะตรวจจับข้อยกเว้นในกลุ่มกระบวนการ
สตริงใน Java - มุมมองภายใน
ที่มา:
สื่อ ผู้เขียนบทความนี้ตัดสินใจที่จะดูรายละเอียดเกี่ยวกับการสร้าง การทำงาน และคุณสมบัติของสตริงใน Java
การสร้าง
สตริงใน Java สามารถสร้างได้สองวิธี: โดยปริยาย เป็นสตริงลิเทอรัล และชัดเจน โดยใช้คีย์เวิร์ด
new ตัวอักษรสตริงคืออักขระที่อยู่ในเครื่องหมายคำพูดคู่
String literal = "Michael Jordan";
String object = new String("Michael Jordan");
แม้ว่าการประกาศทั้งสองจะสร้างวัตถุสตริง แต่ก็มีความแตกต่างในวิธีที่วัตถุทั้งสองนี้อยู่บนหน่วยความจำฮีป
การเป็นตัวแทนภายใน
ก่อนหน้านี้ สตริงถูกจัดเก็บในรูปแบบ
char[]ซึ่งหมายความว่าอักขระแต่ละตัวเป็นองค์ประกอบแยกกันในอาร์เรย์อักขระ เนื่องจากแสดงใน รูปแบบการเข้ารหัสอักขระ
UTF-16ซึ่งหมายความว่าอักขระแต่ละตัวใช้หน่วยความจำสองไบต์ สิ่งนี้ไม่ถูกต้องมากนัก เนื่องจากสถิติการใช้งานแสดงให้เห็นว่าออบเจ็กต์สตริงส่วนใหญ่ประกอบด้วย อักขระ
Latin-1เท่านั้น อักขระ Latin-1 สามารถแสดงได้โดยใช้หน่วยความจำไบต์เดียว ซึ่งสามารถลดการใช้หน่วยความจำลงได้มากถึง 50% คุณลักษณะสตริงภายในใหม่ถูกนำมาใช้เป็นส่วนหนึ่งของการเปิดตัว JDK 9 ตาม
JEP 254ที่เรียกว่า Compact Strings ในรุ่นนี้
char[]ถูกเปลี่ยนเป็น
ไบต์[]และเพิ่มช่องแฟล็กตัวเข้ารหัสเพื่อแสดงการเข้ารหัสที่ใช้ (Latin-1 หรือ UTF-16) หลังจากนี้ การเข้ารหัสจะเกิดขึ้นตามเนื้อหาของสตริง หากค่ามีเฉพาะอักขระ Latin-1 แสดงว่ามีการใช้การเข้ารหัส Latin-1 ( คลาส
StringLatin1 ) หรือใช้การเข้ารหัส UTF-16 ( คลาส
StringUTF16 )
การจัดสรรหน่วยความจำ
ตามที่ระบุไว้ก่อนหน้านี้ มีความแตกต่างในวิธีการจัดสรรหน่วยความจำสำหรับอ็อบเจ็กต์เหล่านี้บนฮีป การใช้คีย์เวิร์ด new ที่ชัดเจนนั้นค่อนข้างตรงไปตรงมา เนื่องจาก JVM สร้างและจัดสรรหน่วยความจำสำหรับตัวแปรบนฮีป ดังนั้น การใช้ตัวอักษรสตริงจึงเป็นไปตามกระบวนการที่เรียกว่าการฝึกงาน String Interning คือกระบวนการใส่สตริงลงในพูล ใช้วิธีการจัดเก็บเพียงสำเนาเดียวของค่าสตริงแต่ละค่า ซึ่งจะต้องไม่เปลี่ยนรูป ค่าส่วนบุคคลจะถูกจัดเก็บไว้ในพูล String Intern พูลนี้เป็น ร้านค้า
Hashtableที่จัดเก็บการอ้างอิงไปยังวัตถุสตริงแต่ละรายการที่สร้างขึ้นโดยใช้ตัวอักษรและแฮชของมัน แม้ว่าค่าสตริงจะอยู่บนฮีป แต่การอ้างอิงสามารถพบได้ในพูลภายใน สามารถตรวจสอบได้อย่างง่ายดายโดยใช้การทดสอบด้านล่าง ที่นี่เรามีตัวแปรสองตัวที่มีค่าเท่ากัน:
String firstName1 = "Michael";
String firstName2 = "Michael";
System.out.println(firstName1 == firstName2);
ในระหว่างการ ประมวล ผลโค้ด เมื่อ JVM พบ
firstName1มันจะค้นหาค่าสตริงในพูลสตริงภายใน
Michael หากไม่พบ รายการใหม่จะถูกสร้างขึ้นสำหรับอ็อบเจ็กต์ในพูลภายใน เมื่อการดำเนินการถึง
firstName2กระบวนการจะทำซ้ำอีกครั้ง และคราวนี้ค่าสามารถพบได้ในพูลตามตัวแปร
firstName1 ด้วยวิธีนี้ แทนที่จะทำซ้ำและสร้างรายการใหม่ ลิงก์เดิมจะถูกส่งกลับ ดังนั้นจึงเป็นไปตามเงื่อนไขความเท่าเทียมกัน ในทางกลับกัน หากตัวแปรที่มีค่า
Michaelถูกสร้างขึ้นโดยใช้คีย์เวิร์ดใหม่ จะไม่มีการฝึกงานเกิดขึ้นและจะไม่เป็นไปตามเงื่อนไขความเท่าเทียมกัน
String firstName3 = new String("Michael");
System.out.println(firstName3 == firstName2);
Interning สามารถใช้กับเมธอด
firstName3 intern()ได้ แม้ว่าปกติจะไม่นิยมใช้วิธีนี้ก็ตาม
firstName3 = firstName3.intern();
System.out.println(firstName3 == firstName2);
การฝึกงานอาจเกิดขึ้นได้เมื่อเชื่อมต่อตัวอักษรสตริงสองตัวโดยใช้ตัว ดำเนินการ
+
String fullName = "Michael Jordan";
System.out.println(fullName == "Michael " + "Jordan");
ต่อไปนี้เราจะเห็นว่า ณ เวลาคอมไพล์ คอมไพลเลอร์จะเพิ่มทั้งตัวอักษรและลบ ตัวดำเนินการ
+ ออก จากนิพจน์เพื่อสร้างเป็นสตริงเดี่ยวดังที่แสดงด้านล่าง ณ รันไทม์ ทั้ง
fullNameและ "added literal" จะถูกรวมเข้าด้วยกัน และเป็นไปตามเงื่อนไขความเท่าเทียมกัน
System.out.println(fullName == "Michael Jordan");
ความเท่าเทียมกัน
จากการทดลองข้างต้น คุณจะเห็นว่ามีเพียงตัวอักษรสตริงเท่านั้นที่อยู่ภายในตามค่าเริ่มต้น อย่างไรก็ตาม แอปพลิเคชัน Java จะไม่เพียงมีตัวอักษรสตริงเท่านั้น เนื่องจากอาจรับสตริงจากแหล่งที่แตกต่างกัน ดังนั้นจึงไม่แนะนำให้ใช้ตัวดำเนินการความเท่าเทียมกันและอาจให้ผลลัพธ์ที่ไม่พึงประสงค์ การทดสอบความเท่าเทียมกันควรทำโดย วิธี
เท่ากับเท่านั้น มันดำเนินการเท่าเทียมกันตามค่าของสตริงมากกว่าที่อยู่หน่วยความจำที่เก็บไว้
System.out.println(firstName1.equals(firstName2));
System.out.println(firstName3.equals(firstName2));
นอกจากนี้ยังมีเวอร์ชันที่ได้รับ การ แก้ไขเล็กน้อยของเมธอดเท่ากับที่เรียกว่า
equalsIgnoreCase อาจมีประโยชน์สำหรับวัตถุประสงค์ที่ไม่คำนึงถึงขนาดตัวพิมพ์
String firstName4 = "miCHAEL";
System.out.println(firstName4.equalsIgnoreCase(firstName1));
ความไม่เปลี่ยนรูป
สตริงไม่เปลี่ยนรูป หมายความว่าสถานะภายในไม่สามารถเปลี่ยนแปลงได้เมื่อสร้างขึ้นแล้ว คุณสามารถเปลี่ยนค่าของตัวแปรได้ แต่เปลี่ยนค่าของสตริงไม่ได้ แต่ละเมธอดของ คลาส
Stringที่เกี่ยวข้องกับการจัดการอ็อบเจ็กต์ (เช่น
concat ,
substring ) ส่งคืนสำเนาใหม่ของค่าแทนที่จะอัปเดตค่าที่มีอยู่
String firstName = "Michael";
String lastName = "Jordan";
firstName.concat(lastName);
System.out.println(firstName);
System.out.println(lastName);
อย่างที่คุณเห็น ไม่มีการ เปลี่ยนแปลง เกิดขึ้นกับตัวแปรใดๆ: ทั้ง
firstNameและ
LastName เมธอดคลาส
สตริงจะไม่เปลี่ยนสถานะภายใน แต่จะสร้างสำเนาใหม่ของผลลัพธ์และส่งกลับผลลัพธ์ดังที่แสดงด้านล่าง
firstName = firstName.concat(lastName);
System.out.println(firstName);
GO TO FULL VERSION