首頁 >Java >java教程 >4 中最有趣的 Java 錯誤

4 中最有趣的 Java 錯誤

Mary-Kate Olsen
Mary-Kate Olsen原創
2025-01-01 09:19:09613瀏覽

2024 年,我們分析了大量項目,並在部落格上分享我們的發現。現在是除夕夜-是時候講喜慶故事了!我們收集了在開源專案中檢測到的最有趣的 Java 錯誤,現在將它們帶給您!

Top most intriguing Java errors in 4

前言

我們長期以來一直秉承發布 PVS-Studio 檢測到的最有趣的 bug 的傳統,但自 2020 年以來就沒有出現與 Java 相關的置頂!這次,我嘗試復興復古風格。我希望您手邊有一條舒適的毯子和一杯熱茶,因為我專門為您挑選了 10 多種有趣的昆蟲。以下是他們的排名:

  • 我的個人意見;
  • 該錯誤的有趣背景;
  • 多樣性、可信度和重要性。

準備好用自己的程式設計智慧迎接 10 個有趣的故事吧——除夕夜很快就要到來了:)

第10名。回到未來

第十位,第一個程式碼片段張開雙臂歡迎我們。

public Builder setPersonalisation(Date date, ....) {
  ....
  final OutputStreamWriter
    out = new OutputStreamWriter(bout, "UTF-8");
  final DateFormat
    format = new SimpleDateFormat("YYYYMMdd");
  out.write(format.format(date));
    ....
}

我忍不住把它放在除夕夜的頂部,因為這段程式碼中的一個錯誤可以讓我們更快地到達下一年:)猜猜這個錯誤是從哪裡來的?

讓我們來看看傳遞給 SimpleDateFormat 建構子的參數。看起來有效嗎?如果我們幾乎傳遞任何日期,例如撰寫本文的日期 (10/12/2024),程式碼將傳回正確的值 20241210。

但是,如果我們傳遞 29/12/2024,它將返回 20251229,從而巧妙地提前進入新年。順便說一句,時光倒流也是可行的。

發生這種情況是因為 SimpleDateFormat 參數中的 Y 字元代表基於週數的年份。簡而言之,當一周至少包含新年的四天時,該一周被視為第一週。所以,如果我們的一周從週日開始,我們就可以提前三天進入新的一年。

要修復此問題,只需將大寫 Y 替換為小寫 y 即可。想了解更多嗎?我們特別寫了一整篇文章來討論這個主題。

這是針對此錯誤的 PVS-Studio 警告:

V6122 偵測到使用「Y」(週年)模式:它可能打算使用「y」(年)。 SkeinParameters.java 246

由於週數的具體情況,因此測試並不是發現此錯誤的最佳方法。那麼為什麼這樣一個話題性的錯誤會出現在最後呢?原因是該警告不是來自 Bouncy Castle 的實際版本,而是來自我們的測試基地。舊的資源仍然存在,而且這個錯誤已經修復了很長時間。這是來自過去的致敬,又是一次時光旅行:)

第9名。 “看來不行”

第九位,我們收到 GeoServer 分析的警告:

@Test
public void testStore() {
  Properties newProps = dao.toProperties();

  // ....
  Assert.assertEquals(newProps.size(), props.size());
  for (Object key : newProps.keySet()) {
    Object newValue = newProps.get(key);
    Object oldValue = newProps.get(key);              // <=
    Assert.assertEquals(newValue, oldValue);
  }
}

這是 PVS-Studio 警告:

V6027 變數「newValue」、「oldValue」透過呼叫相同函數進行初始化。這可能是錯誤或未最佳化的程式碼。 DataAccessRuleDAOTest.java 110、DataAccessRuleDAOTest.java 111

這樣的錯誤有什麼有趣的?讓我來揭示這四個點背後隱藏的是什麼:

public Builder setPersonalisation(Date date, ....) {
  ....
  final OutputStreamWriter
    out = new OutputStreamWriter(bout, "UTF-8");
  final DateFormat
    format = new SimpleDateFormat("YYYYMMdd");
  out.write(format.format(date));
    ....
}

有評論稱該程式碼由於某種原因無法運行。說實話,我第一次看到的時候就笑了。

不過,這篇評論相當含糊。測試很可能是故意以這種方式編寫的,以防止在比較失敗時出現故障。然而,該程式碼已經處於這種狀態十多年了,這引發了一些問題。這種模糊性就是我沒有將其排名更高的原因。

第8名。腳中彈

如果我們不能將 JBullet 中的 bug 稱為“搬起石頭砸自己的腳”,我不知道哪些可以這樣稱呼。這是文章中的一個錯誤:

@Test
public void testStore() {
  Properties newProps = dao.toProperties();

  // ....
  Assert.assertEquals(newProps.size(), props.size());
  for (Object key : newProps.keySet()) {
    Object newValue = newProps.get(key);
    Object oldValue = newProps.get(key);              // <=
    Assert.assertEquals(newValue, oldValue);
  }
}

我認為我們甚至不需要 PVS-Studio 警告來發現錯誤所在。無論如何,以防萬一,這裡是:

V6026 該值已指派給「proxy1」變數。 HashedOverlappingPairCache.java 233

是的,這是一個令人尷尬的簡單錯誤。不過,這種簡單性讓它變得更加搞笑。儘管如此,它還是有自己的故事。

JBullet 庫是 C/C 子彈庫的移植,那裡有類似的功能:

@Test
public void testStore() {
  Properties newProps = dao.toProperties();

  // properties equality does not seem to work...
  Assert.assertEquals(newProps.size(), props.size());
  for (Object key : newProps.keySet()) {
    Object newValue = newProps.get(key);
    Object oldValue = newProps.get(key);
    Assert.assertEquals(newValue, oldValue);
  }
}

很容易看出這段程式碼寫得正確。從 gitblame 來看,原來寫的是正確的。原來是程式碼從一種語言移植到另一種語言時出現了錯誤。

由於其驚人的樸實加上豐富的歷史,我將這個警告評為第八名。我希望你喜歡這個搬起石頭砸自己腳的 bug 原來是與 C 語言相關的。

第七名。即使是偉大的數學家也會犯錯

誠然,下一個警告出於多種原因溫暖了我的心。以下是 GeoGebra 檢查的程式碼片段:

@Override
public BroadphasePair findPair(BroadphaseProxy proxy0, BroadphaseProxy proxy1) {
  BulletStats.gFindPairs++;
  if (proxy0.getUid() > proxy1.getUid()) {
    BroadphaseProxy tmp = proxy0;
    proxy0 = proxy1;
    proxy1 = proxy0;
  }
  ....
}

嘗試自己找出錯誤!為了不讓你們偷看,我把警告和解釋隱藏在劇透裡了。

答案
首先,我們來看看PVS-Studio的警告:

V6107 正在使用常數 0.7071067811865。結果值可能不準確。考慮使用 Math.sqrt(0.5)。 DrawAngle.java 303

事實上,0.7071067811865 並不是什麼神奇的數字——它只是 0.5 平方根的四捨五入結果。但這種精度損失有多嚴重呢? GeoGebra 是一款為數學家量身訂製的軟體,額外的精確度似乎並沒有什麼壞處。

為什麼我這麼喜歡這個bug?

首先,在會議上,我經常要求與會者在其他程式碼片段中找到類似的錯誤。當錯誤隱藏在常數中時,看著他們仔細分析程式碼總是很有趣。

其次,這是我為 Java 分析器實作的第一個診斷規則。這就是為什麼我無法抗拒將它放在頂部的原因——即使意識到了偏見——我把它放在第七位:)

第六名。這個模式不起作用

以下警告是我從基於 DBeaver 檢查的第一篇文章中獲取的,可能不會立即引起我們的注意。這是一個程式碼片段:

public Builder setPersonalisation(Date date, ....) {
  ....
  final OutputStreamWriter
    out = new OutputStreamWriter(bout, "UTF-8");
  final DateFormat
    format = new SimpleDateFormat("YYYYMMdd");
  out.write(format.format(date));
    ....
}

以下是 PVS-Studio 分析器偵測到的內容:

V6082 不安全的雙重檢查鎖定。該欄位應聲明為易失性的。 TaskImpl.java 59、TaskImpl.java317

雖然這個特定的警告沒有什麼特別的,但我仍然覺得它非常有趣。關鍵是所應用的雙重檢查鎖定模式不起作用。有什麼訣竅呢?這在 20 年前是相關的:)

如果您想了解有關該主題的更多信息,我建議您閱讀全文。但現在,讓我給您一個快速總結。

雙重檢查鎖定模式用於在多執行緒環境中實現延遲初始化。在「重量級」檢查之前,會在沒有同步區塊的情況下執行「輕量級」檢查。只有當兩項檢查都通過時才會建立資源。

但是,在這種方法中,物件建立是非原子,並且處理器和編譯器可以更改操作順序。因此,另一個執行緒可能會意外收到部分建立的物件並開始使用它,這可能會導致不正確的行為。這個錯誤可能很少發生,因此調試對開發人員來說將是一個巨大的挑戰。

這裡有一個變化:這種模式直到 JDK 5 才起作用。從 JDK 5 開始,由於 happens-before 原則,引入了 volatile 關鍵字來解決重新排序操作的潛在問題。分析器警告我們應該要添加此關鍵字。

但是,無論如何,最好避免這種模式。從那時起,硬體和 JVM 效能已經取得了長足的進步,並且同步操作不再那麼慢了。然而,不正確地實現 DCL 模式仍然是一個常見的陷阱,可能會產生上述的嚴重後果。這證實了我們的分析器在舊專案中仍然發現此類疏忽錯誤的事實。

第5名。微觀優化

第五名是另一個 DBeaver 警告,我們專門寫了一篇文章。我們來看看:

@Test
public void testStore() {
  Properties newProps = dao.toProperties();

  // ....
  Assert.assertEquals(newProps.size(), props.size());
  for (Object key : newProps.keySet()) {
    Object newValue = newProps.get(key);
    Object oldValue = newProps.get(key);              // <=
    Assert.assertEquals(newValue, oldValue);
  }
}

這裡有個解釋:

V6030 無論左側運算元的值為何,都會呼叫「&」運算子右邊的方法。也許,最好使用“&&”。 ExasolTableColumnManager.java 79、DB2TableColumnManager.java 77

開發人員將邏輯 && 與位元 & 混淆了。它們具有不同的行為:表達式中的條件在位元 AND 之後不會終止。 短路求值 不適用於位元 AND。因此,即使 exasolTableBase != null 將傳回 false,執行緒也會到達 exasolTableBase.getClass() 並導致 NPE。

好吧,這只是一個錯字,讓我們繼續吧,好嗎? DBeaver 有很多這樣的警告。很多。許多都是相對無害的,但對於好奇的讀者,我在下面留下了一些例子:

使用不會導致錯誤的位元運算
ExasolSecurityPolicy.java:
public Builder setPersonalisation(Date date, ....) {
  ....
  final OutputStreamWriter
    out = new OutputStreamWriter(bout, "UTF-8");
  final DateFormat
    format = new SimpleDateFormat("YYYYMMdd");
  out.write(format.format(date));
    ....
}

ExasolConnectionManager.java:

@Test
public void testStore() {
  Properties newProps = dao.toProperties();

  // ....
  Assert.assertEquals(newProps.size(), props.size());
  for (Object key : newProps.keySet()) {
    Object newValue = newProps.get(key);
    Object oldValue = newProps.get(key);              // <=
    Assert.assertEquals(newValue, oldValue);
  }
}

ExasolDataSource.java:

@Test
public void testStore() {
  Properties newProps = dao.toProperties();

  // properties equality does not seem to work...
  Assert.assertEquals(newProps.size(), props.size());
  for (Object key : newProps.keySet()) {
    Object newValue = newProps.get(key);
    Object oldValue = newProps.get(key);
    Assert.assertEquals(newValue, oldValue);
  }
}

深入挖掘後,我的團隊假設開發人員可能一直在嘗試對效能進行微觀最佳化。想了解完整情況,你可以看看我們的文章——這裡我總結一下。

關鍵點是位元運算不依賴分支預測,與邏輯運算相比,可能允許更快的執行。

令我們驚訝的是,一個本土基準測試支持了這個說法:

Top most intriguing Java errors in 4

圖表說明了每種操作類型所需的時間。如果我們相信它,位元運算似乎比邏輯運算快 40%。

我為什麼要提出這個主題?強調潛在微觀優化的成本。

首先,開發人員發明分支預測是有原因的-放棄它的成本太高。因此,基準測試可能運行得更快,因為值具有常態分佈,而在實際情況下不太可能觀察到。

第二,放棄短路評估機制會導致成本大幅上升。如果我們看一下上面劇透中的第三個範例,我們可以看到最快的 contains 操作並不是一直執行而不是立即停止。

第三,我們從本章開始就全權處理這類錯誤。

總體而言,我發現微優化價格的警示故事足以進入我們的前五名。

第四名。不行的話測試也不會落下

自動化測試通常被認為是防止各種錯誤的最終保障。然而,時不時地,我很想問:「誰自己測試這些測試?」來自 GeoServer 檢查的另一個警告再次證明了這一點。這是一個程式碼片段:

@Override
public BroadphasePair findPair(BroadphaseProxy proxy0, BroadphaseProxy proxy1) {
  BulletStats.gFindPairs++;
  if (proxy0.getUid() > proxy1.getUid()) {
    BroadphaseProxy tmp = proxy0;
    proxy0 = proxy1;
    proxy1 = proxy0;
  }
  ....
}

PVS-Studio 警告:

V6060 在驗證「e」引用是否為 null 之前,已使用該引用。 ResourceAccessManagerWCSTest.java 186、ResourceAccessManagerWCSTest.java 193

乍一看,這個警告似乎不是分析器最令人興奮的警告,因為 V6060 經常是針對冗餘程式碼發出的。然而,我承諾我會根據他們的吸引力來選擇提名。所以,這個案例遠比看起來有趣。

最初,測試邏輯可能看起來是錯誤的,因為 e 變數是從 catch 運算子取得的,並且進一步保持不變,因此它永遠不會為 null。我們可以進行雜散編輯,並將 if(e == nul) 條件的 then 分支刪除為無法到達。然而,那是完全錯誤的。你找到竅門了嗎?

關鍵在於包含異常物件的程式碼多了一個變量,它就是一個se。它的值會在循環體內改變。所以,我們很容易猜測,條件中應該要有se變量,而不是e。

這個錯誤會導致then分支永遠不會被執行,所以我們不知道有沒有異常。更糟的是,在程式碼審查中很難注意到這樣的錯誤,因為變數名稱非常相似。

從這個故事可以汲取兩個智慧:

  1. 清楚地命名變量,即使在測試中也是如此。不然更容易犯這樣的錯誤;
  2. 測試不足以保證專案質量,因為它們也可能包含錯誤。因此,它會在應用程式內留下漏洞。

由於提供瞭如此寶貴的課程,我將此警告授予第四名。

第三名。祝各位調試愉快

前三名獲勝者屬於 NetBeans 檢查的警告。之前的程式碼片段比較緊湊,我們看一下比較長的程式碼片段:

public Builder setPersonalisation(Date date, ....) {
  ....
  final OutputStreamWriter
    out = new OutputStreamWriter(bout, "UTF-8");
  final DateFormat
    format = new SimpleDateFormat("YYYYMMdd");
  out.write(format.format(date));
    ....
}

最後一次,試著自己找錯誤-我會等...

Top most intriguing Java errors in 4

正在尋找?

不錯!那些只在表達式 iDesc.neighbor != null || 中發現錯誤的人iDesc.index == iDesc.index,很遺憾,你輸了:)

當然,有一個問題,但對於排名第一的問題來說還不夠有趣。是的,這裡有兩個錯誤,我欺騙了你一點。但是沒有一點惡作劇的假期怎麼算呢? :)

分析器偵測到此處的 i^i 表達式有錯誤,並發出以下警告:

V6001 在「^」運算子的左邊和右邊有相同的子運算式「i」。 LayoutFeeder.java 3897

異或運算沒有任何意義,因為兩個相同值的異或將永遠為零。為了快速回顧一下,這裡是 XOR 的真值表:

a b a^b
0 0 0
0 1 1
1 0 1
1 1 0

換句話說,只有當運算元不同時,運算才會為真。我們將擁有相同的所有位,因為值是相同的。

為什麼我這麼喜歡這個bug?有 i^1 操作,看起來與 i^i 幾乎相同。因此,在程式碼審查中很容易錯過這個錯誤,因為我們已經在上面看到了正確的 i^1。

我不了解你,但這讓我想起了著名的:

public Builder setPersonalisation(Date date, ....) {
  ....
  final OutputStreamWriter
    out = new OutputStreamWriter(bout, "UTF-8");
  final DateFormat
    format = new SimpleDateFormat("YYYYMMdd");
  out.write(format.format(date));
    ....
}

否則,很難解釋它是如何進入程式碼的——除非我們用一個簡單的拼字錯誤來忽略這個無聊的版本。如果您確實發現了該錯誤,請拍拍自己的背,或在評論中分享您的偵探技巧:)

第二名。當模式失敗時

我已經顯示了第一篇和第三篇 DBeaver 文章中的錯誤,跳過第二篇文章。我糾正了——以下內容僅來自第二篇文章。

PVS-Studio 分析器不喜歡從 TextWithOpen 類別的建構函式呼叫 isBinaryContents,該類別在子類別中被重寫:

@Test
public void testStore() {
  Properties newProps = dao.toProperties();

  // ....
  Assert.assertEquals(newProps.size(), props.size());
  for (Object key : newProps.keySet()) {
    Object newValue = newProps.get(key);
    Object oldValue = newProps.get(key);              // <=
    Assert.assertEquals(newValue, oldValue);
  }
}

那又怎樣?它被覆蓋了——不過,沒什麼大不了的。這看起來像是代碼味道,沒什麼關鍵的。至少,我以前是這麼認為的。我專門寫了一篇文章來闡述我與這個錯誤的鬥爭。

TextWithOpen 有許多子類,其中之一就是 TextWithOpenFile。在那裡,該方法實際上被重寫,它會傳回一個超類別沒有的字段,而不是 false:

@Test
public void testStore() {
  Properties newProps = dao.toProperties();

  // properties equality does not seem to work...
  Assert.assertEquals(newProps.size(), props.size());
  for (Object key : newProps.keySet()) {
    Object newValue = newProps.get(key);
    Object oldValue = newProps.get(key);
    Assert.assertEquals(newValue, oldValue);
  }
}

還有疑問嗎?這個類別的建構子是什麼樣的?

@Override
public BroadphasePair findPair(BroadphaseProxy proxy0, BroadphaseProxy proxy1) {
  BulletStats.gFindPairs++;
  if (proxy0.getUid() > proxy1.getUid()) {
    BroadphaseProxy tmp = proxy0;
    proxy0 = proxy1;
    proxy1 = proxy0;
  }
  ....
}

注意到了嗎?呼叫超類別建構函式後,將初始化二進位欄位。然而,有一個對 isBinaryContents 方法的調用,它引用了子類別欄位!

Top most intriguing Java errors in 4

這是 PVS-Studio 警告:

V6052 在「TextWithOpen」父類別建構子中呼叫重寫的「isBinaryContents」方法可能會導致使用未初始化的資料。檢查字段:二進制。 TextWithOpenFile.java(77), TextWithOpen.java 59

這是一張相當有趣的圖片。乍一看,開發人員似乎遵循了最佳實踐:避免無法維護的義大利麵條式程式碼,並嘗試透過模板方法模式實現規範的 OOP。但是,即使在實現這樣簡單的模式時,我們也可能會犯錯誤,而這就是所發生的情況。在我看來,這種(錯誤的)簡單之美是穩居第二的。

第一名。一個錯誤抵消了另一個錯誤

高興吧!舞台第一名!競爭很激烈,但必須做出選擇。經過深思熟慮,我決定接受 NetBeans 檢查中的警告。讓我介紹一下最終的程式碼片段:

public Builder setPersonalisation(Date date, ....) {
  ....
  final OutputStreamWriter
    out = new OutputStreamWriter(bout, "UTF-8");
  final DateFormat
    format = new SimpleDateFormat("YYYYMMdd");
  out.write(format.format(date));
    ....
}

我確信不可能一眼就能發現這樣的錯誤──當然,除非你自己犯過這個錯誤。我不會讓您久等的——這是 PVS-Studio 警告:

V6009 緩衝區容量使用字元值設定為「47」。最有可能的是,“/”符號應該放置在緩衝區中。忽略UnignoreCommand.java 107

事實上,這個錯誤非常簡單:StringBuilder 建構子沒有接受 char 的重載。那麼呼叫什麼構造函數呢?開發者顯然認為會呼叫一個接受 String 的重載,然後 StringBuilder 的初始值就是這個斜線。

但是,會發生隱式型別轉換,並呼叫接受 int 的型別建構函式。在我們的例子中,它代表 StringBuilder 的初始大小。將 char 作為參數傳遞不會在功能上影響任何內容,因為它不會包含在最終字串中。如果超出初始大小,它只會自行增加,不會導致異常或其他副作用。

但是等等,我提到了兩個錯誤,不是嗎?第二個在哪裡,它們是如何連結的?為了發現這一點,我們必須讀入方法體並了解這段程式碼的作用。

它產生檔案或目錄的絕對路徑。根據程式碼,產生的路徑應如下所示:

  • 對於檔案:/folder1/file
  • 對於目錄:/folder1/folder/.

程式碼看起來非常正確。這就是問題所在。程式碼確實可以正常工作:)但是如果我們透過用字串替換字元來修復錯誤,我們將得到這個而不是正確的結果:

  • /資料夾1/檔案/;
  • /資料夾1/資料夾//

換句話說,我們會在字串末尾得到一個額外的斜杠。它將位於末尾,因為上面的程式碼每次都會將新文字添加到行的開頭。

因此,第二個錯誤是這個斜槓根本作為參數傳遞給建構子。但是,我不會低估這樣的錯誤,因為如果有人決定在不檢查的情況下用字串替換字符,可能會出現問題。

這就是錯誤頂部的第一個位置轉到正確工作的程式碼的方式。新年奇蹟,你期待什麼? :)

結論

我希望您喜歡閱讀我的錯誤故事。如果您有任何特別的故事讓您印象深刻,或者您有調整排名的建議,請隨時在評論中分享您的想法,我會在下次記住它們:)

如果您對其他語言感興趣,我邀請您在此處查看 2024 年最熱門的 C# 錯誤 - 請繼續關注新的熱門錯誤!

所有這些錯誤都是用PVS-Studio分析器偵測到的,最新版本(7.34)剛剛發布!您可以透過此連結嘗試。

要繼續關注程式碼品質的新文章,我們邀請您訂閱:

  • PVS-Studio X(推特);
  • 我們的每月文章摘要;

新年快樂!

以上是4 中最有趣的 Java 錯誤的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn