JavaRush /จาวาบล็อก /Random-TH /คอฟฟี่เบรค #146. 5 ข้อผิดพลาดที่ 99% ของ Java Developer ท...

คอฟฟี่เบรค #146. 5 ข้อผิดพลาดที่ 99% ของ Java Developer ทำ สตริงใน Java - มุมมองภายใน

เผยแพร่ในกลุ่ม

5 ข้อผิดพลาดที่ 99% ของ Java Developer ทำ

ที่มา: สื่อ ในโพสต์นี้ คุณจะได้เรียนรู้เกี่ยวกับข้อผิดพลาดทั่วไปที่ Java Developer จำนวนมากทำ คอฟฟี่เบรค #146.  5 ข้อผิดพลาดที่ 99% ของ Java Developer ทำ  สตริงใน Java - มุมมองภายใน - 1ในฐานะโปรแกรมเมอร์ Java ฉันรู้ว่าการใช้เวลามากมายไปกับการแก้ไขจุดบกพร่องในโค้ดของคุณนั้นแย่แค่ไหน บางครั้งอาจใช้เวลาหลายชั่วโมง อย่างไรก็ตาม มีข้อผิดพลาดมากมายเกิดขึ้นเนื่องจากการที่นักพัฒนาละเลยกฎพื้นฐาน นั่นคือข้อผิดพลาดระดับต่ำมาก วันนี้เราจะมาดูข้อผิดพลาดทั่วไปในการเขียนโค้ดแล้วอธิบายวิธีแก้ไข ฉันหวังว่านี่จะช่วยคุณหลีกเลี่ยงปัญหาในการทำงานประจำวันของคุณได้

การเปรียบเทียบวัตถุโดยใช้ Objects.equals

ฉันคิดว่าคุณคุ้นเคยกับวิธีนี้ นักพัฒนาหลายคนใช้มันบ่อยๆ เทคนิคนี้ที่นำมาใช้ใน JDK 7 ช่วยให้คุณเปรียบเทียบวัตถุได้อย่างรวดเร็ว และหลีกเลี่ยงการตรวจสอบตัวชี้ว่างที่น่ารำคาญได้อย่างมีประสิทธิภาพ แต่บางครั้งวิธีนี้ก็ใช้ไม่ถูกต้อง นี่คือสิ่งที่ฉันหมายถึง:
Long longValue = 123L;
System.out.println(longValue==123); //true
System.out.println(Objects.equals(longValue,123)); //false
เหตุใดการแทนที่==ด้วย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));//2022-12-31 08:00:00
ซึ่งใช้ รูปแบบ 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());// the size is 2
ผู้อ่านที่เอาใจใส่บางคนควรจะสามารถเดาสาเหตุของความล้มเหลวได้ HashSetใช้รหัสแฮชเพื่อเข้าถึงตารางแฮชและใช้วิธีการเท่ากับเพื่อกำหนดว่าวัตถุเท่ากันหรือไม่ หากวัตถุที่ผู้ใช้กำหนดไม่ได้แทนที่วิธี hashcode และ วิธี เท่ากับ วิธี การดังกล่าวจะใช้วิธี hashcode และ วิธี เท่ากับ ของวัตถุหลักเป็นค่าเริ่มต้น ซึ่งจะ ทำให้ HashSetสันนิษฐานว่าเป็นวัตถุสองชิ้นที่แตกต่างกัน ส่งผลให้การขจัดข้อมูลซ้ำซ้อนล้มเหลว

กำจัดด้ายสระน้ำที่ "กิน"

ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.submit(()->{
            //do something
            double result = 10/0;
        });
โค้ดด้านบนจำลองสถานการณ์ที่มีข้อยกเว้นเกิดขึ้นในเธรดพูล รหัสธุรกิจต้องยอมรับสถานการณ์ต่างๆ ดังนั้นจึงมีโอกาสมากที่จะส่งRuntimeException ด้วยเหตุผลบาง ประการ แต่หากไม่มีการจัดการพิเศษที่นี่ ข้อยกเว้นนี้จะถูก "กิน" โดยเธรดพูล และคุณจะไม่มีวิธีตรวจสอบสาเหตุของข้อยกเว้นด้วยซ้ำ ดังนั้นจึงเป็นการดีที่สุดที่จะตรวจจับข้อยกเว้นในกลุ่มกระบวนการ

สตริงใน Java - มุมมองภายใน

ที่มา:สื่อ ผู้เขียนบทความนี้ตัดสินใจที่จะดูรายละเอียดเกี่ยวกับการสร้าง การทำงาน และคุณสมบัติของสตริงใน Java คอฟฟี่เบรค #146.  5 ข้อผิดพลาดที่ 99% ของ Java Developer ทำ  สตริงใน Java - มุมมองภายใน - 2

การสร้าง

สตริงใน 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);             //true
ในระหว่างการ ประมวล ผลโค้ด เมื่อ JVM พบfirstName1มันจะค้นหาค่าสตริงในพูลสตริงภายในMichael หากไม่พบ รายการใหม่จะถูกสร้างขึ้นสำหรับอ็อบเจ็กต์ในพูลภายใน เมื่อการดำเนินการถึงfirstName2กระบวนการจะทำซ้ำอีกครั้ง และคราวนี้ค่าสามารถพบได้ในพูลตามตัวแปรfirstName1 ด้วยวิธีนี้ แทนที่จะทำซ้ำและสร้างรายการใหม่ ลิงก์เดิมจะถูกส่งกลับ ดังนั้นจึงเป็นไปตามเงื่อนไขความเท่าเทียมกัน ในทางกลับกัน หากตัวแปรที่มีค่าMichaelถูกสร้างขึ้นโดยใช้คีย์เวิร์ดใหม่ จะไม่มีการฝึกงานเกิดขึ้นและจะไม่เป็นไปตามเงื่อนไขความเท่าเทียมกัน
String firstName3 = new String("Michael");
System.out.println(firstName3 == firstName2);           //false
Interning สามารถใช้กับเมธอดfirstName3 intern()ได้ แม้ว่าปกติจะไม่นิยมใช้วิธีนี้ก็ตาม
firstName3 = firstName3.intern();                      //Interning
System.out.println(firstName3 == firstName2);          //true
การฝึกงานอาจเกิดขึ้นได้เมื่อเชื่อมต่อตัวอักษรสตริงสองตัวโดยใช้ตัว ดำเนินการ +
String fullName = "Michael Jordan";
System.out.println(fullName == "Michael " + "Jordan");     //true
ต่อไปนี้เราจะเห็นว่า ณ เวลาคอมไพล์ คอมไพลเลอร์จะเพิ่มทั้งตัวอักษรและลบ ตัวดำเนินการ + ออก จากนิพจน์เพื่อสร้างเป็นสตริงเดี่ยวดังที่แสดงด้านล่าง ณ รันไทม์ ทั้งfullNameและ "added literal" จะถูกรวมเข้าด้วยกัน และเป็นไปตามเงื่อนไขความเท่าเทียมกัน
//After Compilation
System.out.println(fullName == "Michael Jordan");

ความเท่าเทียมกัน

จากการทดลองข้างต้น คุณจะเห็นว่ามีเพียงตัวอักษรสตริงเท่านั้นที่อยู่ภายในตามค่าเริ่มต้น อย่างไรก็ตาม แอปพลิเคชัน Java จะไม่เพียงมีตัวอักษรสตริงเท่านั้น เนื่องจากอาจรับสตริงจากแหล่งที่แตกต่างกัน ดังนั้นจึงไม่แนะนำให้ใช้ตัวดำเนินการความเท่าเทียมกันและอาจให้ผลลัพธ์ที่ไม่พึงประสงค์ การทดสอบความเท่าเทียมกันควรทำโดย วิธี เท่ากับเท่านั้น มันดำเนินการเท่าเทียมกันตามค่าของสตริงมากกว่าที่อยู่หน่วยความจำที่เก็บไว้
System.out.println(firstName1.equals(firstName2));       //true
System.out.println(firstName3.equals(firstName2));       //true
นอกจากนี้ยังมีเวอร์ชันที่ได้รับ การ แก้ไขเล็กน้อยของเมธอดเท่ากับที่เรียกว่าequalsIgnoreCase อาจมีประโยชน์สำหรับวัตถุประสงค์ที่ไม่คำนึงถึงขนาดตัวพิมพ์
String firstName4 = "miCHAEL";
System.out.println(firstName4.equalsIgnoreCase(firstName1));  //true

ความไม่เปลี่ยนรูป

สตริงไม่เปลี่ยนรูป หมายความว่าสถานะภายในไม่สามารถเปลี่ยนแปลงได้เมื่อสร้างขึ้นแล้ว คุณสามารถเปลี่ยนค่าของตัวแปรได้ แต่เปลี่ยนค่าของสตริงไม่ได้ แต่ละเมธอดของ คลาส Stringที่เกี่ยวข้องกับการจัดการอ็อบเจ็กต์ (เช่นconcat , substring ) ส่งคืนสำเนาใหม่ของค่าแทนที่จะอัปเดตค่าที่มีอยู่
String firstName  = "Michael";
String lastName   = "Jordan";
firstName.concat(lastName);

System.out.println(firstName);                       //Michael
System.out.println(lastName);                        //Jordan
อย่างที่คุณเห็น ไม่มีการ เปลี่ยนแปลง เกิดขึ้นกับตัวแปรใดๆ: ทั้งfirstNameและLastName เมธอดคลาสสตริงจะไม่เปลี่ยนสถานะภายใน แต่จะสร้างสำเนาใหม่ของผลลัพธ์และส่งกลับผลลัพธ์ดังที่แสดงด้านล่าง
firstName = firstName.concat(lastName);

System.out.println(firstName);                      //MichaelJordan
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION