JavaRush /Blog Java /Random-MS /Coffee break #146. 5 kesilapan yang dilakukan oleh 99% pe...

Coffee break #146. 5 kesilapan yang dilakukan oleh 99% pembangun Java. Rentetan dalam Java - pandangan dalam

Diterbitkan dalam kumpulan

5 kesilapan yang dilakukan oleh 99% pembangun Java

Sumber: Sederhana Dalam siaran ini, anda akan belajar tentang kesilapan paling biasa yang dilakukan oleh banyak pembangun Java. Coffee break #146.  5 kesilapan yang dilakukan oleh 99% pembangun Java.  Rentetan dalam Java - pandangan dalam - 1Sebagai pengaturcara Java, saya tahu betapa teruknya menghabiskan banyak masa membetulkan pepijat dalam kod anda. Kadang-kadang ini mengambil masa beberapa jam. Walau bagaimanapun, banyak ralat muncul disebabkan oleh fakta bahawa pembangun mengabaikan peraturan asas - iaitu, ini adalah ralat peringkat rendah. Hari ini kita akan melihat beberapa kesilapan pengekodan biasa dan kemudian menerangkan cara membetulkannya. Saya harap ini membantu anda mengelakkan masalah dalam kerja harian anda.

Membandingkan objek menggunakan Objek.sama

Saya menganggap anda sudah biasa dengan kaedah ini. Ramai pemaju sering menggunakannya. Teknik ini, yang diperkenalkan dalam JDK 7, membantu anda membandingkan objek dengan cepat dan mengelakkan pemeriksaan penuding nol yang menjengkelkan. Tetapi kaedah ini kadang-kadang digunakan secara tidak betul. Inilah yang saya maksudkan:
Long longValue = 123L;
System.out.println(longValue==123); //true
System.out.println(Objects.equals(longValue,123)); //false
Mengapa menggantikan == dengan Objects.equals() menghasilkan hasil yang salah? Ini kerana pengkompil == akan memperoleh jenis data asas yang sepadan dengan jenis pembungkusan longValue dan kemudian membandingkannya dengan jenis data asas tersebut. Ini bersamaan dengan pengkompil secara automatik menukar pemalar kepada jenis data perbandingan asas. Selepas menggunakan kaedah Objects.equals() , jenis data asas lalai pemalar pengkompil ialah int . Di bawah ialah kod sumber untuk Objects.equals() dengan a.equals(b) menggunakan Long.equals() dan menentukan jenis objek. Ini berlaku kerana pengkompil menganggap bahawa pemalar adalah jenis int , jadi hasil perbandingan mestilah palsu.
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;
    }
Mengetahui sebabnya, membetulkan ralat adalah sangat mudah. Hanya isytiharkan jenis data pemalar, seperti Objects.equals(longValue,123L) . Masalah di atas tidak akan timbul jika logiknya tegas. Apa yang perlu kita lakukan ialah mengikut peraturan pengaturcaraan yang jelas.

Format tarikh salah

Dalam pembangunan harian, anda sering perlu menukar tarikh, tetapi ramai orang menggunakan format yang salah, yang membawa kepada perkara yang tidak dijangka. Berikut ialah contoh:
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
Ini menggunakan format YYYY-MM-dd untuk menukar tarikh dari 2021 kepada 2022. Awak tak patut buat macam tu. kenapa? Ini kerana corak Java DateTimeFormatter "YYYY" adalah berdasarkan standard ISO-8601, yang mentakrifkan tahun sebagai hari Khamis setiap minggu. Tetapi 31 Disember 2021 jatuh pada hari Jumaat, jadi program itu salah menunjukkan 2022. Untuk mengelakkan ini, anda mesti menggunakan format yyyy-MM-dd untuk memformat tarikh . Ralat ini jarang berlaku, hanya dengan ketibaan tahun baru. Tetapi di syarikat saya ia menyebabkan kegagalan pengeluaran.

Menggunakan ThreadLocal dalam ThreadPool

Jika anda mencipta pembolehubah ThreadLocal , maka utas yang mengakses pembolehubah itu akan mencipta pembolehubah setempat utas. Dengan cara ini anda boleh mengelakkan isu keselamatan benang. Walau bagaimanapun, jika anda menggunakan ThreadLocal pada kolam benang , anda perlu berhati-hati. Kod anda mungkin menghasilkan hasil yang tidak dijangka. Untuk contoh mudah, katakan kami mempunyai platform e-dagang dan pengguna perlu menghantar e-mel untuk mengesahkan pembelian produk yang lengkap.
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);
        });
    }
Jika kami menggunakan ThreadLocal untuk menyimpan maklumat pengguna, ralat tersembunyi akan muncul. Oleh kerana kumpulan benang digunakan dan benang boleh digunakan semula, apabila menggunakan ThreadLocal untuk mendapatkan maklumat pengguna, ia mungkin tersilap memaparkan maklumat orang lain. Untuk menyelesaikan masalah ini, anda harus menggunakan sesi.

Gunakan HashSet untuk mengalih keluar data pendua

Apabila mengekod, kita sering memerlukan penyahduplikasian. Apabila anda memikirkan deduplikasi, perkara pertama yang difikirkan oleh ramai orang ialah menggunakan HashSet . Walau bagaimanapun, penggunaan HashSet yang cuai boleh menyebabkan penyahduplikasian gagal.
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
Sesetengah pembaca yang prihatin sepatutnya dapat meneka sebab kegagalan itu. HashSet menggunakan kod cincang untuk mengakses jadual cincang dan menggunakan kaedah sama untuk menentukan sama ada objek adalah sama. Jika objek yang ditentukan pengguna tidak mengatasi kaedah kod cincang dan sama dengan kaedah , maka kaedah kod cincang dan kaedah sama dengan objek induk akan digunakan secara lalai. Ini akan menyebabkan HashSet menganggap bahawa ia adalah dua objek berbeza, menyebabkan penyahduaan gagal.

Menghapuskan benang kolam yang "dimakan".

ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.submit(()->{
            //do something
            double result = 10/0;
        });
Kod di atas mensimulasikan senario di mana pengecualian dilemparkan ke dalam kumpulan benang. Kod perniagaan mesti menganggap pelbagai situasi, jadi kemungkinan besar ia akan membuang RuntimeException atas sebab tertentu . Tetapi jika tiada pengendalian khas di sini, maka pengecualian ini akan "dimakan" oleh kumpulan benang. Dan anda tidak akan mempunyai cara untuk menyemak punca pengecualian. Oleh itu, yang terbaik adalah untuk menangkap pengecualian dalam kumpulan proses.

Rentetan dalam Java - pandangan dalam

Sumber: Sederhana Pengarang artikel ini memutuskan untuk melihat secara terperinci penciptaan, fungsi dan ciri rentetan di Jawa. Coffee break #146.  5 kesilapan yang dilakukan oleh 99% pembangun Java.  Rentetan dalam Java - pandangan dalam - 2

Ciptaan

Rentetan dalam Java boleh dibuat dalam dua cara berbeza: secara tersirat, sebagai rentetan literal dan secara eksplisit, menggunakan kata kunci baharu . Huruf rentetan ialah aksara yang disertakan dalam petikan berganda.
String literal   = "Michael Jordan";
String object    = new String("Michael Jordan");
Walaupun kedua-dua pengisytiharan mencipta objek rentetan, terdapat perbezaan dalam cara kedua-dua objek ini terletak pada memori timbunan.

Perwakilan dalaman

Sebelum ini, rentetan disimpan dalam bentuk char[] , bermakna setiap aksara ialah elemen berasingan dalam tatasusunan aksara. Memandangkan ia diwakili dalam format pengekodan aksara UTF-16 , ini bermakna setiap aksara menggunakan dua bait memori. Ini tidak begitu betul, kerana statistik penggunaan menunjukkan bahawa kebanyakan objek rentetan terdiri daripada aksara Latin-1 sahaja . Aksara Latin-1 boleh diwakili menggunakan satu bait memori, yang boleh mengurangkan penggunaan memori dengan ketara—sebanyak 50%. Ciri rentetan dalaman baharu telah dilaksanakan sebagai sebahagian daripada keluaran JDK 9 berdasarkan JEP 254 yang dipanggil Compact Strings. Dalam keluaran ini, char[] ditukar kepada byte[] dan medan bendera pengekod telah ditambahkan untuk mewakili pengekodan yang digunakan (Latin-1 atau UTF-16). Selepas ini, pengekodan berlaku berdasarkan kandungan rentetan. Jika nilai hanya mengandungi aksara Latin-1, maka pengekodan Latin-1 digunakan ( kelas StringLatin1 ) atau pengekodan UTF-16 digunakan ( kelas StringUTF16 ).

Peruntukan ingatan

Seperti yang dinyatakan sebelum ini, terdapat perbezaan dalam cara memori diperuntukkan untuk objek ini pada timbunan. Menggunakan kata kunci baharu yang jelas adalah agak mudah kerana JVM mencipta dan memperuntukkan memori untuk pembolehubah pada timbunan. Oleh itu, menggunakan literal rentetan mengikuti proses yang dipanggil interning. String interning ialah proses memasukkan tali ke dalam kolam. Ia menggunakan kaedah untuk menyimpan hanya satu salinan bagi setiap nilai rentetan individu, yang mesti tidak boleh diubah. Nilai individu disimpan dalam kumpulan String Intern. Kolam ini ialah kedai Hashtable yang menyimpan rujukan kepada setiap objek rentetan yang dibuat menggunakan literal dan cincangnya. Walaupun nilai rentetan berada pada timbunan, rujukannya boleh didapati dalam kolam dalaman. Ini boleh disahkan dengan mudah menggunakan percubaan di bawah. Di sini kita mempunyai dua pembolehubah dengan nilai yang sama:
String firstName1   = "Michael";
String firstName2   = "Michael";
System.out.println(firstName1 == firstName2);             //true
Semasa pelaksanaan kod, apabila JVM menemui firstName1 , ia mencari nilai rentetan dalam kumpulan rentetan dalaman Michael . Jika ia tidak dapat menemuinya, maka entri baharu dibuat untuk objek dalam kolam dalaman. Apabila pelaksanaan mencapai firstName2 , proses berulang lagi dan kali ini nilai boleh ditemui dalam kumpulan berdasarkan pembolehubah firstName1 . Dengan cara ini, bukannya menduplikasi dan mencipta entri baharu, pautan yang sama dikembalikan. Oleh itu, syarat kesamarataan dipenuhi. Sebaliknya, jika pembolehubah dengan nilai Michael dibuat menggunakan kata kunci baharu, tiada interning berlaku dan syarat kesamarataan tidak dipenuhi.
String firstName3 = new String("Michael");
System.out.println(firstName3 == firstName2);           //false
Interning boleh digunakan dengan kaedah firstName3 intern() , walaupun ini biasanya tidak diutamakan.
firstName3 = firstName3.intern();                      //Interning
System.out.println(firstName3 == firstName2);          //true
Interning juga boleh berlaku apabila menggabungkan dua literal rentetan menggunakan operator + .
String fullName = "Michael Jordan";
System.out.println(fullName == "Michael " + "Jordan");     //true
Di sini kita melihat bahawa pada masa penyusunan, pengkompil menambah kedua-dua literal dan mengalih keluar operator + daripada ungkapan untuk membentuk satu rentetan seperti yang ditunjukkan di bawah. Pada masa jalanan, kedua-dua fullName dan "tertambah literal" dimasukkan dan syarat kesamarataan dipenuhi.
//After Compilation
System.out.println(fullName == "Michael Jordan");

Kesaksamaan

Daripada percubaan di atas, anda dapat melihat bahawa hanya literal rentetan yang dimasukkan secara lalai. Walau bagaimanapun, aplikasi Java pastinya tidak hanya mempunyai literal rentetan, kerana ia mungkin menerima rentetan daripada sumber yang berbeza. Oleh itu, menggunakan operator kesamarataan tidak disyorkan dan mungkin menghasilkan hasil yang tidak diingini. Ujian kesamarataan hanya boleh dilakukan dengan kaedah equals . Ia melakukan kesamaan berdasarkan nilai rentetan dan bukannya alamat memori tempat ia disimpan.
System.out.println(firstName1.equals(firstName2));       //true
System.out.println(firstName3.equals(firstName2));       //true
Terdapat juga versi yang diubah suai sedikit bagi kaedah equals yang dipanggil equalsIgnoreCase . Ia mungkin berguna untuk tujuan tidak peka huruf besar-besaran.
String firstName4 = "miCHAEL";
System.out.println(firstName4.equalsIgnoreCase(firstName1));  //true

Ketidakbolehubahan

Rentetan tidak boleh diubah, bermakna keadaan dalamannya tidak boleh diubah setelah ia dicipta. Anda boleh menukar nilai pembolehubah, tetapi bukan nilai rentetan itu sendiri. Setiap kaedah kelas String yang berurusan dengan memanipulasi objek (contohnya, concat , substring ) mengembalikan salinan nilai baharu dan bukannya mengemas kini nilai sedia ada.
String firstName  = "Michael";
String lastName   = "Jordan";
firstName.concat(lastName);

System.out.println(firstName);                       //Michael
System.out.println(lastName);                        //Jordan
Seperti yang anda lihat, tiada perubahan berlaku pada mana-mana pembolehubah: firstName mahupun lastName . Kaedah kelas rentetan tidak mengubah keadaan dalaman, mereka mencipta salinan baru hasil dan mengembalikan hasilnya seperti yang ditunjukkan di bawah.
firstName = firstName.concat(lastName);

System.out.println(firstName);                      //MichaelJordan
Komen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION