JavaRush /Blog Java /Random-VI /Nghỉ giải lao #146. 5 sai lầm mà 99% lập trình viên Java ...

Nghỉ giải lao #146. 5 sai lầm mà 99% lập trình viên Java mắc phải Chuỗi trong Java - chế độ xem bên trong

Xuất bản trong nhóm

5 sai lầm mà 99% lập trình viên Java mắc phải

Nguồn: Medium Trong bài đăng này, bạn sẽ tìm hiểu về những lỗi phổ biến nhất mà nhiều nhà phát triển Java mắc phải. Nghỉ giải lao #146.  5 sai lầm mà 99% lập trình viên Java mắc phải  Chuỗi trong Java - chế độ xem bên trong - 1Là một lập trình viên Java, tôi biết việc dành nhiều thời gian để sửa lỗi trong mã của mình sẽ tệ đến mức nào. Đôi khi việc này mất vài giờ. Tuy nhiên, nhiều lỗi xuất hiện do nhà phát triển bỏ qua các quy tắc cơ bản - tức là đây là những lỗi ở mức độ rất thấp. Hôm nay chúng ta sẽ xem xét một số lỗi mã hóa phổ biến và sau đó giải thích cách khắc phục chúng. Tôi hy vọng điều này sẽ giúp bạn tránh được những vấn đề trong công việc hàng ngày.

So sánh các đối tượng bằng Objects.equals

Tôi cho rằng bạn đã quen thuộc với phương pháp này. Nhiều nhà phát triển sử dụng nó thường xuyên. Kỹ thuật này, được giới thiệu trong JDK 7, giúp bạn nhanh chóng so sánh các đối tượng và tránh việc kiểm tra con trỏ null gây khó chịu một cách hiệu quả. Nhưng phương pháp này đôi khi được sử dụng không chính xác. Đây là ý tôi:
Long longValue = 123L;
System.out.println(longValue==123); //true
System.out.println(Objects.equals(longValue,123)); //false
Tại sao việc thay thế == bằng Objects.equals() lại tạo ra kết quả sai? Điều này là do trình biên dịch == sẽ lấy kiểu dữ liệu cơ bản tương ứng với kiểu đóng gói longValue và sau đó so sánh nó với kiểu dữ liệu cơ bản đó. Điều này tương đương với việc trình biên dịch tự động chuyển đổi các hằng số thành kiểu dữ liệu so sánh cơ bản. Sau khi sử dụng phương thức Objects.equals() , kiểu dữ liệu cơ sở mặc định của hằng số trình biên dịch là int . Dưới đây là mã nguồn của Objects.equals() trong đó a.equals(b) sử dụng Long.equals() và xác định loại đối tượng. Điều này xảy ra vì trình biên dịch giả định rằng hằng số thuộc loại int , do đó kết quả so sánh phải sai.
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;
    }
Biết được nguyên nhân, việc sửa lỗi rất đơn giản. Chỉ cần khai báo kiểu dữ liệu của các hằng số, như Objects.equals(longValue,123L) . Những vấn đề trên sẽ không phát sinh nếu logic chặt chẽ. Những gì chúng ta cần làm là tuân theo các quy tắc lập trình rõ ràng.

Định dạng ngày không chính xác

Trong quá trình phát triển hàng ngày, bạn thường xuyên phải thay đổi ngày tháng nhưng nhiều người sử dụng sai định dạng dẫn đến những điều không mong muốn. Đây là một ví dụ:
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
Điều này sử dụng định dạng YYYY-MM-dd để thay đổi ngày từ 2021 thành 2022. Bạn không nên làm điều đó. Tại sao? Điều này là do mẫu Java DateTimeFormatter “YYYY” dựa trên tiêu chuẩn ISO-8601, tiêu chuẩn này xác định năm là thứ Năm hàng tuần. Nhưng ngày 31 tháng 12 năm 2021 rơi vào thứ Sáu nên chương trình chỉ sai năm 2022. Để tránh điều này, bạn phải sử dụng định dạng yyyy-MM-dd để định dạng ngày tháng . Lỗi này xảy ra không thường xuyên, chỉ khi năm mới đến. Nhưng ở công ty của tôi, nó đã gây ra lỗi sản xuất.

Sử dụng ThreadLocal trong ThreadPool

Nếu bạn tạo một biến ThreadLocal thì một luồng truy cập vào biến đó sẽ tạo một biến cục bộ của luồng. Bằng cách này bạn có thể tránh được các vấn đề về an toàn luồng. Tuy nhiên, nếu bạn đang sử dụng ThreadLocal trên nhóm luồng , bạn cần cẩn thận. Mã của bạn có thể tạo ra kết quả không mong muốn. Ví dụ đơn giản, giả sử chúng tôi có một nền tảng thương mại điện tử và người dùng cần gửi email để xác nhận việc mua sản phẩm đã hoàn tất.
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);
        });
    }
Nếu chúng ta sử dụng ThreadLocal để lưu thông tin người dùng sẽ xuất hiện lỗi ẩn. Vì một nhóm chủ đề được sử dụng và các chủ đề có thể được sử dụng lại nên khi sử dụng ThreadLocal để lấy thông tin người dùng, nó có thể hiển thị sai thông tin của người khác. Để giải quyết vấn đề này, bạn nên sử dụng Session.

Sử dụng HashSet để loại bỏ dữ liệu trùng lặp

Khi viết mã, chúng ta thường có nhu cầu chống trùng lặp. Khi bạn nghĩ đến việc loại bỏ trùng lặp, điều đầu tiên nhiều người nghĩ đến là sử dụng HashSet . Tuy nhiên, việc sử dụng HashSet bất cẩn có thể khiến quá trình chống trùng lặp không thành công.
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
Một số độc giả chú ý sẽ có thể đoán được lý do thất bại. HashSet sử dụng mã băm để truy cập bảng băm và sử dụng phương thức bằng để xác định xem các đối tượng có bằng nhau hay không. Nếu đối tượng do người dùng định nghĩa không ghi đè phương thức mã băm và phương thức bằng thì phương thức mã băm và phương thức bằng của đối tượng cha sẽ được sử dụng theo mặc định. Điều này sẽ khiến HashSet cho rằng chúng là hai đối tượng khác nhau, khiến cho việc loại bỏ trùng lặp không thành công.

Loại bỏ một luồng hồ bơi "đã ăn"

ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.submit(()->{
            //do something
            double result = 10/0;
        });
Đoạn mã trên mô phỏng một kịch bản trong đó một ngoại lệ được đưa vào nhóm luồng. Mã doanh nghiệp phải giả định nhiều tình huống khác nhau, vì vậy rất có thể nó sẽ ném ra RuntimeException vì lý do nào đó . Nhưng nếu không có cách xử lý đặc biệt nào ở đây thì ngoại lệ này sẽ bị thread pool “ăn thịt”. Và bạn thậm chí sẽ không có cách nào để kiểm tra nguyên nhân của ngoại lệ. Vì vậy, cách tốt nhất là nắm bắt các ngoại lệ trong nhóm quy trình.

Chuỗi trong Java - chế độ xem bên trong

Nguồn: Medium Tác giả của bài viết này quyết định xem xét chi tiết quá trình tạo, chức năng và tính năng của chuỗi trong Java. Nghỉ giải lao #146.  5 sai lầm mà 99% lập trình viên Java mắc phải  Chuỗi trong Java - chế độ xem bên trong - 2

Sự sáng tạo

Một chuỗi trong Java có thể được tạo theo hai cách khác nhau: ngầm, dưới dạng chuỗi ký tự và rõ ràng, sử dụng từ khóa mới . Chuỗi ký tự là các ký tự được đặt trong dấu ngoặc kép.
String literal   = "Michael Jordan";
String object    = new String("Michael Jordan");
Mặc dù cả hai khai báo đều tạo ra một đối tượng chuỗi, nhưng có sự khác biệt về cách cả hai đối tượng này nằm trên bộ nhớ heap.

Đại diện nội bộ

Trước đây, các chuỗi được lưu trữ ở dạng char[] , nghĩa là mỗi ký tự là một phần tử riêng biệt trong mảng ký tự. Vì chúng được thể hiện ở định dạng mã hóa ký tự UTF-16 , điều này có nghĩa là mỗi ký tự chiếm hai byte bộ nhớ. Điều này không chính xác lắm vì số liệu thống kê sử dụng cho thấy hầu hết các đối tượng chuỗi chỉ bao gồm các ký tự Latin-1 . Các ký tự Latin-1 có thể được biểu diễn bằng một byte bộ nhớ, điều này có thể làm giảm đáng kể mức sử dụng bộ nhớ—đến 50%. Một tính năng chuỗi nội bộ mới đã được triển khai như một phần của bản phát hành JDK 9 dựa trên JEP 254 có tên là Chuỗi nhỏ gọn. Trong bản phát hành này, char[] đã được đổi thành byte[] và trường cờ bộ mã hóa đã được thêm vào để thể hiện mã hóa được sử dụng (Latin-1 hoặc UTF-16). Sau đó, quá trình mã hóa diễn ra dựa trên nội dung của chuỗi. Nếu giá trị chỉ chứa các ký tự Latin-1 thì mã hóa Latin-1 sẽ được sử dụng (lớp StringLatin1 ) hoặc mã hóa UTF-16 được sử dụng ( lớp StringUTF16 ).

Cấp phát bộ nhớ

Như đã nêu trước đó, có sự khác biệt trong cách phân bổ bộ nhớ cho các đối tượng này trên heap. Việc sử dụng từ khóa new rõ ràng khá đơn giản vì JVM tạo và phân bổ bộ nhớ cho biến trên vùng nhớ heap. Do đó, việc sử dụng một chuỗi ký tự phải tuân theo một quy trình gọi là thực tập. Thực tập chuỗi là quá trình đưa các chuỗi vào một nhóm. Nó sử dụng phương pháp chỉ lưu trữ một bản sao của mỗi giá trị chuỗi riêng lẻ, không thể thay đổi được. Các giá trị riêng lẻ được lưu trữ trong nhóm String Intern. Nhóm này là một kho lưu trữ Hashtable lưu trữ tham chiếu đến từng đối tượng chuỗi được tạo bằng cách sử dụng hằng và hàm băm của nó. Mặc dù giá trị chuỗi nằm trên heap nhưng tham chiếu của nó có thể được tìm thấy trong vùng nội bộ. Điều này có thể được xác minh dễ dàng bằng thử nghiệm dưới đây. Ở đây chúng ta có hai biến có cùng giá trị:
String firstName1   = "Michael";
String firstName2   = "Michael";
System.out.println(firstName1 == firstName2);             //true
Trong quá trình thực thi mã, khi JVM gặp firstName1 , nó sẽ tra cứu giá trị chuỗi trong nhóm chuỗi bên trong Michael . Nếu không thể tìm thấy thì một mục mới sẽ được tạo cho đối tượng trong nhóm nội bộ. Khi quá trình thực thi đạt tới firstName2 , quy trình sẽ lặp lại một lần nữa và lần này giá trị có thể được tìm thấy trong nhóm dựa trên biến firstName1 . Bằng cách này, thay vì sao chép và tạo một mục mới, liên kết tương tự sẽ được trả về. Do đó điều kiện đẳng thức được thỏa mãn. Mặt khác, nếu một biến có giá trị Michael được tạo bằng từ khóa mới, thì không có sự thực tập nào xảy ra và điều kiện đẳng thức không được thỏa mãn.
String firstName3 = new String("Michael");
System.out.println(firstName3 == firstName2);           //false
Thực tập có thể được sử dụng với phương thức firstName3 intern() , mặc dù điều này thường không được ưa thích.
firstName3 = firstName3.intern();                      //Interning
System.out.println(firstName3 == firstName2);          //true
Việc thực hiện cũng có thể xảy ra khi nối hai chuỗi ký tự bằng cách sử dụng toán tử + .
String fullName = "Michael Jordan";
System.out.println(fullName == "Michael " + "Jordan");     //true
Ở đây chúng ta thấy rằng tại thời điểm biên dịch, trình biên dịch sẽ thêm cả hai chữ và loại bỏ toán tử + khỏi biểu thức để tạo thành một chuỗi như hiển thị bên dưới. Trong thời gian chạy, cả fullName và "chữ được thêm vào" đều được thực hiện và điều kiện đẳng thức được thỏa mãn.
//After Compilation
System.out.println(fullName == "Michael Jordan");

Bình đẳng

Từ các thử nghiệm ở trên, bạn có thể thấy rằng theo mặc định, chỉ có các chuỗi ký tự được chèn vào. Tuy nhiên, một ứng dụng Java chắc chắn sẽ không chỉ có các chuỗi ký tự, vì nó có thể nhận các chuỗi từ các nguồn khác nhau. Do đó, việc sử dụng toán tử đẳng thức không được khuyến khích và có thể tạo ra kết quả không mong muốn. Kiểm tra tính bằng chỉ nên được thực hiện bằng phương pháp bằng . Nó thực hiện sự bình đẳng dựa trên giá trị của chuỗi chứ không phải địa chỉ bộ nhớ nơi nó được lưu trữ.
System.out.println(firstName1.equals(firstName2));       //true
System.out.println(firstName3.equals(firstName2));       //true
Ngoài ra còn có một phiên bản được sửa đổi một chút của phương thức bằng được gọi là bằngIgnoreCase . Nó có thể hữu ích cho các mục đích không phân biệt chữ hoa chữ thường.
String firstName4 = "miCHAEL";
System.out.println(firstName4.equalsIgnoreCase(firstName1));  //true

Tính bất biến

Chuỗi là bất biến, nghĩa là trạng thái bên trong của chúng không thể thay đổi sau khi chúng được tạo. Bạn có thể thay đổi giá trị của một biến, nhưng không thể thay đổi giá trị của chính chuỗi đó. Mỗi phương thức của lớp String xử lý việc thao tác một đối tượng (ví dụ: concat , substring ) trả về một bản sao mới của giá trị thay vì cập nhật giá trị hiện có.
String firstName  = "Michael";
String lastName   = "Jordan";
firstName.concat(lastName);

System.out.println(firstName);                       //Michael
System.out.println(lastName);                        //Jordan
Như bạn có thể thấy, không có thay đổi nào xảy ra với bất kỳ biến nào: cả firstName lẫn LastName . Các phương thức lớp chuỗi không thay đổi trạng thái bên trong, chúng tạo một bản sao mới của kết quả và trả về kết quả như dưới đây.
firstName = firstName.concat(lastName);

System.out.println(firstName);                      //MichaelJordan
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION