App下載

超越常見陷阱:Java開發(fā)人員五大致命錯(cuò)誤

孫尚香 2023-12-08 10:06:53 瀏覽數(shù) (1504)
反饋

Java是一種廣泛使用的編程語言,它具有跨平臺(tái)、面向?qū)ο蟆⒏咝阅艿忍攸c(diǎn)。但即使對(duì)于經(jīng)驗(yàn)豐富的開發(fā)人員,也常常會(huì)犯一些致命的錯(cuò)誤。這些錯(cuò)誤可能導(dǎo)致代碼質(zhì)量下降、性能問題或安全漏洞。本文將揭示Java開發(fā)人員常犯的五大致命錯(cuò)誤,并提供了寶貴的建議,助您避免陷入這些錯(cuò)誤,提升代碼質(zhì)量和開發(fā)效率。

1. 使用Objects.equals比較對(duì)象

?Objects.equals?是Java 7提供的一個(gè)靜態(tài)方法,它可以用來比較兩個(gè)對(duì)象是否相等,同時(shí)避免空指針異常。這個(gè)方法看起來很方便,但是如果使用不當(dāng),可能會(huì)導(dǎo)致意想不到的結(jié)果。例如,下面的代碼中,使用?Objects.equals?比較一個(gè)Long類型的變量和一個(gè)int類型的常量,結(jié)果卻是false。

Long longValue = 123L;
System.out.println(longValue == 123); // true
System.out.println(Objects.equals(longValue, 123)); // false

?Objects.equals?方法內(nèi)部會(huì)先判斷兩個(gè)參數(shù)是否為同一對(duì)象,如果不是,再調(diào)用第一個(gè)參數(shù)的?equals?方法比較第二個(gè)參數(shù)。而Long類的?equals?方法會(huì)先判斷參數(shù)是否為L(zhǎng)ong類型,如果不是,直接返回?false?。所以,當(dāng)我們使用?Objects.equals?比較一個(gè)Long類型的變量和一個(gè)int類型的常量時(shí),實(shí)際上是調(diào)用了Long類的?equals?方法,而這個(gè)方法會(huì)認(rèn)為兩個(gè)參數(shù)類型不同,所以返回false。要避免這個(gè)錯(cuò)誤,我們需要注意以下幾點(diǎn):

  • 當(dāng)比較基本類型和包裝類型時(shí),盡量使用?==?運(yùn)算符,因?yàn)樗鼤?huì)自動(dòng)進(jìn)行拆箱和類型轉(zhuǎn)換,而不會(huì)出現(xiàn)類型不匹配的問題。
  • 當(dāng)比較兩個(gè)包裝類型時(shí),盡量保證它們的類型一致,或者使用相應(yīng)的?parse?方法將它們轉(zhuǎn)換為基本類型再比較。
  • 當(dāng)比較自定義類型時(shí),盡量重寫?equals?方法和?hashCode?方法,以實(shí)現(xiàn)合理的相等判斷邏輯。

2. 日期格式錯(cuò)誤

在Java中,我們經(jīng)常需要對(duì)日期進(jìn)行格式化,以便在不同的場(chǎng)景中顯示或存儲(chǔ)。為了實(shí)現(xiàn)日期格式化,我們通常會(huì)使用?DateTimeFormatter?類,它可以根據(jù)指定的模式將日期轉(zhuǎn)換為字符串,或者將字符串轉(zhuǎn)換為日期。然而,如果我們使用錯(cuò)誤的模式,可能會(huì)導(dǎo)致日期格式化出現(xiàn)錯(cuò)誤。例如,下面的代碼中,使用YYYY-MM-dd模式格式化一個(gè)Instant對(duì)象,結(jié)果卻得到了錯(cuò)誤的年份。

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

?DateTimeFormatter?類中的模式字母YYYY和yyyy有細(xì)微的差別。它們都表示年份,但是yyyy表示日歷年,而YYYY表示周年。日歷年是按照公歷的規(guī)則劃分的,而周年是按照ISO 8601標(biāo)準(zhǔn)劃分的,它的第一周是包含1月4日的那一周,而最后一周是包含12月28日的那一周。所以,當(dāng)我們使用YYYY-MM-dd模式格式化一個(gè)Instant對(duì)象時(shí),實(shí)際上是使用了周年的年份,而不是日歷年的年份。而12月31日屬于下一年的第一周,所以得到了錯(cuò)誤的年份。要避免這個(gè)錯(cuò)誤,我們需要注意以下幾點(diǎn):

  • 當(dāng)使用?DateTimeFormatter?類格式化日期時(shí),盡量使用yyyy表示年份,而不是YYYY,除非我們確實(shí)需要使用周年的概念。
  • 當(dāng)使用?DateTimeFormatter?類解析字符串時(shí),盡量保證字符串的格式和模式的格式一致,否則可能會(huì)出現(xiàn)解析異常或錯(cuò)誤的日期。
  • 當(dāng)使用?DateTimeFormatter?類進(jìn)行日期轉(zhuǎn)換時(shí),盡量指定時(shí)區(qū),否則可能會(huì)出現(xiàn)時(shí)差的問題。

3. 在ThreadPool中使用ThreadLocal

?ThreadLocal?是一種特殊的變量,它可以為每個(gè)線程提供一個(gè)獨(dú)立的副本,從而實(shí)現(xiàn)線程間的隔離。使用?ThreadLocal?可以避免一些線程安全的問題,也可以提高一些性能。然而,如果我們?cè)谑褂镁€程池的情況下使用?ThreadLocal?,就要小心了,因?yàn)檫@可能會(huì)導(dǎo)致一些意想不到的結(jié)果。例如,下面的代碼中,使用?ThreadLocal?保存用戶信息,然后在線程池中執(zhí)行一個(gè)任務(wù),發(fā)送郵件給用戶。

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);
    });
}

這段代碼看起來沒有什么問題,但是實(shí)際上有一個(gè)隱藏的bug。因?yàn)槲覀兪褂昧司€程池,線程是可以復(fù)用的,所以在使用?ThreadLocal?獲取用戶信息的時(shí)候,很可能會(huì)誤獲取到別人的信息。這是因?yàn)?ThreadLocal?的副本是綁定在線程上的,而不是綁定在任務(wù)上的,所以當(dāng)一個(gè)線程執(zhí)行完一個(gè)任務(wù)后,它的?ThreadLocal?的副本并不會(huì)被清除,而是會(huì)被下一個(gè)任務(wù)使用。這樣就可能導(dǎo)致數(shù)據(jù)混亂或安全漏洞。要避免這個(gè)錯(cuò)誤,我們需要注意以下幾點(diǎn):

  • 當(dāng)使用?ThreadLocal?時(shí),盡量在每次使用完后調(diào)用?remove?方法,以清除線程的副本,避免對(duì)下一個(gè)任務(wù)造成影響。
  • 當(dāng)使用線程池時(shí),盡量不要使用?ThreadLocal?,而是使用其他的方式來傳遞數(shù)據(jù),例如使用參數(shù)或返回值。
  • 當(dāng)使用線程池時(shí),盡量使用自定義的線程工廠,以便在創(chuàng)建線程時(shí)設(shè)置一些初始化的操作,或者在回收線程時(shí)設(shè)置一些清理的操作。

4. 使用HashSet去除重復(fù)數(shù)據(jù)

在編程的時(shí)候,我們經(jīng)常會(huì)遇到去重的需求,即從一個(gè)集合中去除重復(fù)的元素,只保留唯一的元素。為了實(shí)現(xiàn)去重,我們通常會(huì)使用?HashSet?,它是一種基于哈希表的集合,它可以保證元素的唯一性,同時(shí)具有較高的查詢效率。然而,如果我們使用?HashSet?去重時(shí)不注意一些細(xì)節(jié),可能會(huì)導(dǎo)致去重失敗。例如,下面的代碼中,使用?HashSet?去重一個(gè)?User?對(duì)象的列表,結(jié)果卻沒有去除重復(fù)的對(duì)象。

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?的去重機(jī)制是基于對(duì)象的?hashCode?方法和?equals?方法的。當(dāng)我們向?HashSet?中添加一個(gè)對(duì)象時(shí),它會(huì)先計(jì)算對(duì)象的哈希碼,然后根據(jù)哈希碼找到對(duì)應(yīng)的桶,再在桶中遍歷元素,使用?equals?方法判斷是否有相同

5.在List中循環(huán)刪除元素

這是一個(gè)很常見的錯(cuò)誤,很多開發(fā)人員都會(huì)在使用?List?的?foreach?循環(huán)時(shí),試圖在循環(huán)體中刪除元素,這樣做會(huì)導(dǎo)致?ConcurrentModificationException?異常,因?yàn)樵诘^程中修改了集合的結(jié)構(gòu)。如果要在循環(huán)中刪除元素,應(yīng)該使用迭代器的r?emove?方法,或者使用Java 8提供的?removeIf?方法,或者使用一個(gè)臨時(shí)的集合來存儲(chǔ)要?jiǎng)h除的元素,然后在循環(huán)結(jié)束后再進(jìn)行刪除。例如:

List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");

// 錯(cuò)誤的做法,使用foreach循環(huán)刪除元素
for (String s : list) {
    if (s.equals("b")) {
        list.remove(s); // 拋出ConcurrentModificationException異常
}
}

// 正確的做法,使用迭代器刪除元素
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("b")) {
        it.remove(); // 安全的刪除元素
    }
}

// 正確的做法,使用Java 8的removeIf方法刪除元素
list.removeIf(s -> s.equals("b")); // 使用lambda表達(dá)式刪除元素

// 正確的做法,使用臨時(shí)集合刪除元素
List<String> temp = new ArrayList<String>();
for (String s : list) {
    if (s.equals("b")) {
        temp.add(s); // 將要?jiǎng)h除的元素添加到臨時(shí)集合中
    }
}

// 一次性刪除所有元素
list.removeAll(temp); 

總結(jié)

在Java開發(fā)中,避免常見錯(cuò)誤是提高代碼質(zhì)量和開發(fā)效率的關(guān)鍵。本文揭示了Java開發(fā)人員常犯的五大致命錯(cuò)誤,并提供了寶貴的建議。遵循良好的命名和代碼風(fēng)格,您將能夠更好地避免這些錯(cuò)誤,提升代碼質(zhì)量并取得更高的開發(fā)效率。

1698630578111788

如果你對(duì)編程知識(shí)和相關(guān)職業(yè)感興趣,歡迎訪問編程獅官網(wǎng)(http://m.hgci.cn/)。在編程獅,我們提供廣泛的技術(shù)教程、文章和資源,幫助你在技術(shù)領(lǐng)域不斷成長(zhǎng)。無論你是剛剛起步還是已經(jīng)擁有多年經(jīng)驗(yàn),我們都有適合你的內(nèi)容,助你取得成功。

0 人點(diǎn)贊