JavaRush /Java Blog /Random-TW /喝咖啡休息#146。99% 的 Java 開發人員都會犯的 5 個錯誤。Java 中的字串 - 內部視圖

喝咖啡休息#146。99% 的 Java 開發人員都會犯的 5 個錯誤。Java 中的字串 - 內部視圖

在 Random-TW 群組發布

99% 的 Java 開發人員常犯的 5 個錯誤

來源:Medium 在這篇文章中,您將了解許多 Java 開發人員最常犯的錯誤。 喝咖啡休息#146。 99% 的 Java 開發人員都會犯的 5 個錯誤。 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,因此比較結果必定為 false。
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 標準,該標準將年份定義為每週的星期四。但 2021 年 12 月 31 日是星期五,因此該程序錯誤地指示了 2022 年。為了避免這種情況,您必須使用yyyy-MM-dd格式來格式化日期。這種錯誤很少發生,只有新年到來時才會出現。但在我的公司卻導致了生產失敗。

在ThreadPool中使用ThreadLocal

如果建立一個 ThreadLocal 變量,那麼存取該變數的線程將建立一個線程局部變數。這樣就可以避免執行緒安全問題。但是,如果您在線程池上使用ThreadLocal,則需要小心。您的程式碼可能會產生意外的結果。舉個簡單的例子,假設我們有一個電子商務平台,使用者需要發送電子郵件來確認已完成購買產品。
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使用雜湊碼來存取雜湊表,並使用equals方法來判斷物件是否相等。如果使用者自訂物件沒有重寫hashcode方法和equals方法,那麼預設使用父物件的hashcode方法和equals方法。這將導致HashSet假設它們是兩個不同的對象,從而導致重複資料刪除失敗。

消除「被吃掉」的池串

ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.submit(()->{
            //do something
            double result = 10/0;
        });
上面的程式碼模擬了線程池拋出異常的場景。業務代碼必須假設各種情況,因此很有可能會因為某種原因拋出RuntimeException。但如果這裡沒有特殊處理的話,那麼這個異常就會被線程池「吃掉」。而且您甚至沒有辦法檢查異常的原因。因此,最好在進程池中捕獲異常。

Java 中的字串 - 內部視圖

來源: Medium 本文作者決定詳細了解 Java 中字串的建立、功能和特性。 喝咖啡休息#146。 99% 的 Java 開發人員都會犯的 5 個錯誤。 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實現了一個新的內部字串功能,稱為緊湊字串。在此版本中,char[]變更為byte[],並新增了編碼器標誌欄位來表示所使用的編碼(Latin-1 或 UTF-16)。之後,根據字串的內容進行編碼。如果值僅包含 Latin-1 字符,則使用 Latin-1 編碼(StringLatin1類別)或使用 UTF-16 編碼(StringUTF16類別)。

記憶體分配

如前所述,在堆上為這些物件分配記憶體的方式有所不同。使用顯式 new 關鍵字非常簡單,因為 JVM 在堆上為變數建立並分配記憶體。因此,使用字串文字需要遵循一個稱為實習的過程。字串駐留是將字串放入池中的過程。它使用一種僅儲存每個單獨字串值的一個副本的方法,該副本必須是不可變的。各個值儲存在 String Intern 池中。該池是一個哈希表存儲,它存儲對使用文字及其哈希創建的每個字串物件的參考。雖然字串值在堆上,但它的引用可以在內部池中找到。使用下面的實驗可以輕鬆驗證這一點。這裡我們有兩個具有相同值的變數:
String firstName1   = "Michael";
String firstName2   = "Michael";
System.out.println(firstName1 == firstName2);             //true
在程式碼執行期間,當 JVM 遇到firstName1時,它會在內部字串池Michael中尋找該字串值。如果找不到它,則會在內部池中為該物件建立一個新條目。當執行到firstName2時,該過程再次重複,這次可以根據firstName1變數在池中找到該值。這樣,就不會重複並建立新條目,而是返回相同的連結。因此,滿足平等條件。另一方面,如果使用 new 關鍵字建立值為Michael 的變量,則不會發生駐留且不滿足相等條件。
String firstName3 = new String("Michael");
System.out.println(firstName3 == firstName2);           //false
實習可以與firstName3 intern()方法 一起使用,儘管這通常不是首選。
firstName3 = firstName3.intern();                      //Interning
System.out.println(firstName3 == firstName2);          //true
使用+運算子 連接兩個字串文字時也可能發生實習。
String fullName = "Michael Jordan";
System.out.println(fullName == "Michael " + "Jordan");     //true
在這裡我們看到,在編譯時,編譯器會新增兩個文字並從表達式中刪除+運算子以形成單一字串,如下所示。在執行時,fullName和「新增的文字」都會被保留,並且滿足相等條件。
//After Compilation
System.out.println(fullName == "Michael Jordan");

平等

從上面的實驗中,您可以看到預設僅保留字串文字。然而,Java 應用程式肯定不會只有字串文字,因為它可能會從不同的來源接收字串。因此,不建議使用相等運算符,並且可能會產生不良結果。相等測試只能透過equals方法執行。它根據字串的值而不是儲存字串的記憶體位址來執行相等性。
System.out.println(firstName1.equals(firstName2));       //true
System.out.println(firstName3.equals(firstName2));       //true
equals 方法還有一個稍微修改過的版本,稱為equalsIgnoreCase。它對於不區分大小寫的目的可能很有用。
String firstName4 = "miCHAEL";
System.out.println(firstName4.equalsIgnoreCase(firstName1));  //true

不變性

字串是不可變的,這意味著它們的內部狀態一旦創建就無法更改。您可以更改變數的值,但不能更改字串本身的值。String類別處理物件操作的每個方法(例如concatsubstring)都會傳回值的新副本,而不是更新現有值。
String firstName  = "Michael";
String lastName   = "Jordan";
firstName.concat(lastName);

System.out.println(firstName);                       //Michael
System.out.println(lastName);                        //Jordan
正如您所看到的,任何變數都沒有改變:firstNamelastName都沒有改變。String類別方法不會更改內部狀態,它們會建立結果的新副本並傳回結果,如下所示。
firstName = firstName.concat(lastName);

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