大家好,我是 V 哥。今天看了阿里云開(kāi)發(fā)者社區(qū)關(guān)于 Java 的靈魂拷問(wèn),一線大廠在用 Java 時(shí),都會(huì)考慮哪些問(wèn)題呢,對(duì)于工作多年,又沒(méi)有大廠經(jīng)歷的小伙伴不妨看看,V 哥總結(jié)的這13個(gè)為什么,你都會(huì)哪些?先贊后看,絕不擺爛。
BigDecimal
的 equals
方法在等值比較時(shí)存在一些問(wèn)題,通常不建議直接使用它來(lái)判斷數(shù)值的相等性。下面是主要原因以及推薦的替代方案:
equals
方法比較嚴(yán)格,包含了精度和符號(hào)的比較
BigDecimal.equals
不僅比較數(shù)值本身,還會(huì)比較精度和符號(hào)。例如,BigDecimal
的 equals
方法會(huì)認(rèn)為 1.0
和 1.00
是不同的值,因?yàn)樗鼈兊?scale
不同(即小數(shù)位數(shù)不同)。例如:
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
System.out.println(a.equals(b)); // 輸出 false
盡管 1.0
和 1.00
數(shù)值上是相等的,但 equals
方法會(huì)因?yàn)榫炔煌祷?false
。
equals
方法會(huì)區(qū)分正負(fù)零
在 BigDecimal
中,正零 (0.0
) 和負(fù)零 (-0.0
) 是不相等的,而使用 equals
會(huì)導(dǎo)致 0.0
和 -0.0
被視為不相等。例如:
BigDecimal zero1 = new BigDecimal("0.0");
BigDecimal zero2 = new BigDecimal("-0.0");
System.out.println(zero1.equals(zero2)); // 輸出 false
這可能會(huì)導(dǎo)致誤判,因?yàn)樵诖蠖鄶?shù)業(yè)務(wù)邏輯中,我們認(rèn)為 0.0
和 -0.0
是等值的。
compareTo
方法
為了避免這些問(wèn)題,建議使用 BigDecimal.compareTo
方法。compareTo
方法僅比較數(shù)值的大小,不關(guān)注精度和符號(hào)。因此,在需要判斷兩個(gè) BigDecimal
是否等值時(shí),使用 compareTo
更為合理:
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
System.out.println(a.compareTo(b) == 0); // 輸出 true
在這種情況下,1.0
和 1.00
被視為相等,即使它們的精度不同,compareTo
也會(huì)返回 0
。
equals
方法:它會(huì)考慮精度和符號(hào),容易導(dǎo)致誤判。compareTo
方法:只比較數(shù)值,忽略精度和正負(fù)零的差異,可以實(shí)現(xiàn)更符合業(yè)務(wù)需求的等值比較。在使用 BigDecimal
時(shí),不建議直接使用 double
作為構(gòu)造參數(shù)。這是因?yàn)?double
類型在 Java 中的表示是基于二進(jìn)制浮點(diǎn)數(shù)的,會(huì)引入精度誤差,從而導(dǎo)致不準(zhǔn)確的結(jié)果。例如:
double d = 0.1;
BigDecimal bd = new BigDecimal(d);
System.out.println(bd); // 輸出 0.1000000000000000055511151231257827021181583404541015625
double
使用 IEEE 754 標(biāo)準(zhǔn)表示小數(shù),在二進(jìn)制系統(tǒng)中,像 0.1
這樣的小數(shù)無(wú)法精確表示,導(dǎo)致它在存儲(chǔ)時(shí)會(huì)變成一個(gè)近似值。這個(gè)近似值會(huì)直接傳遞給 BigDecimal
的構(gòu)造方法,從而生成帶有誤差的 BigDecimal
值。double
構(gòu)造 BigDecimal
會(huì)帶來(lái)潛在的誤差積累,從而影響最終的結(jié)果。例如,在多次計(jì)算或累加時(shí),誤差可能不斷放大。BigDecimal
BigDecimal bd = new BigDecimal("0.1");
System.out.println(bd); // 輸出 0.1
BigDecimal.valueOf(double)
方法BigDecimal.valueOf(double)
,該方法會(huì)將 double
轉(zhuǎn)換為 String
表示,然后構(gòu)造 BigDecimal
,從而避免精度損失。 BigDecimal bd = BigDecimal.valueOf(0.1);
System.out.println(bd); // 輸出 0.1
double
構(gòu)造 BigDecimal
,以免引入二進(jìn)制浮點(diǎn)數(shù)的精度誤差。BigDecimal.valueOf(double)
以確保精度。Apache BeanUtils
是一個(gè)早期用于 Java Bean 屬性復(fù)制的工具庫(kù),但在現(xiàn)代 Java 開(kāi)發(fā)中通常不推薦使用它來(lái)進(jìn)行屬性的拷貝,尤其在性能敏感的場(chǎng)景中。原因主要包括以下幾點(diǎn):
Apache BeanUtils.copyProperties()
使用了大量的反射操作,且每次拷貝都需要對(duì)字段、方法進(jìn)行查找和反射調(diào)用。反射機(jī)制雖然靈活,但性能較低,尤其是在大量對(duì)象或頻繁拷貝的場(chǎng)景中,會(huì)產(chǎn)生顯著的性能瓶頸。
相比之下,Spring BeanUtils
或 Apache Commons Lang
的 FieldUtils
等工具經(jīng)過(guò)優(yōu)化,使用了更高效的方式進(jìn)行屬性復(fù)制。在性能要求較高的場(chǎng)合,MapStruct
或 Dozer
等編譯期代碼生成的方式則可以完全避免運(yùn)行時(shí)反射。
BeanUtils.copyProperties
在屬性類型不匹配時(shí)會(huì)隱式地進(jìn)行類型轉(zhuǎn)換。例如,將 String
類型的 "123"
轉(zhuǎn)換為 Integer
,如果轉(zhuǎn)換失敗,會(huì)拋出異常。這種隱式轉(zhuǎn)換在處理數(shù)據(jù)時(shí),可能帶來(lái)不易察覺(jué)的錯(cuò)誤,而且并不總是適合應(yīng)用場(chǎng)景。
在精確的屬性復(fù)制需求下,通常希望類型不匹配時(shí)直接跳過(guò)拷貝,或明確拋出錯(cuò)誤,而不是隱式轉(zhuǎn)換。例如,Spring BeanUtils.copyProperties
不會(huì)進(jìn)行隱式轉(zhuǎn)換,適合嚴(yán)格的屬性匹配場(chǎng)景。
Apache BeanUtils
的 PropertyUtils
組件在執(zhí)行反射操作時(shí)存在一定的安全隱患。歷史上,BeanUtils
的 PropertyUtils
曾有安全漏洞,使惡意用戶可以通過(guò)精心構(gòu)造的輸入利用反射機(jī)制執(zhí)行系統(tǒng)命令或加載惡意類。盡管這些漏洞在現(xiàn)代版本中已得到修復(fù),但該庫(kù)的架構(gòu)和實(shí)現(xiàn)仍較為陳舊,難以應(yīng)對(duì)更高的安全需求。
BeanUtils.copyProperties
僅支持淺拷貝,即只能復(fù)制對(duì)象的一級(jí)屬性,無(wú)法遞歸地對(duì)嵌套對(duì)象進(jìn)行復(fù)制。如果對(duì)象包含了復(fù)雜的嵌套結(jié)構(gòu),使用 BeanUtils.copyProperties
很容易出現(xiàn)意外行為或數(shù)據(jù)丟失。像 MapStruct
或 Dozer
這樣的工具則提供對(duì)嵌套對(duì)象的深層復(fù)制能力,更適合復(fù)雜對(duì)象的深度拷貝需求。
BeanUtils.copyProperties()
BeanUtils.copyProperties()
提供了更優(yōu)的性能和更好的類型安全性。它不做類型轉(zhuǎn)換,且提供了方便的過(guò)濾器用于選擇性拷貝屬性。MapStruct
是基于注解的對(duì)象映射框架,支持編譯期生成代碼,完全避免了反射的性能開(kāi)銷,且支持復(fù)雜對(duì)象、嵌套屬性的深度拷貝,是性能要求較高的首選。Dozer
支持更靈活的映射配置和深拷貝,適合對(duì)象結(jié)構(gòu)復(fù)雜的情況。它可以處理嵌套屬性映射、類型轉(zhuǎn)換,且具有較好的自定義能力。
Apache BeanUtils.copyProperties
不適合現(xiàn)代 Java 開(kāi)發(fā)的性能、安全性和靈活性要求,推薦使用更高效、安全、靈活的框架(如 Spring BeanUtils
、MapStruct
等)來(lái)代替。
在日期格式化中,必須使用 y
而不是 Y
來(lái)表示年份,這是因?yàn)?y
和 Y
在 Java 和其他日期格式化工具中代表不同的含義:
y
表示日歷年(Calendar Year)y
是標(biāo)準(zhǔn)的表示年份的字符,表示的是通常意義上的公歷年,比如 2024
表示的就是這一年的年份。使用 y
時(shí),日期格式化工具會(huì)準(zhǔn)確地格式化出對(duì)應(yīng)的年份數(shù)值: SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
System.out.println(sdf.format(new Date())); // 輸出: 2024-11-10
Y
表示星期年(Week Year)Y
表示的是“星期年”或稱“ISO周年”(ISO week-numbering year),它是一種基于ISO周數(shù)的年份表示方式。這種表示法根據(jù)每年的第一個(gè)星期一所在的周來(lái)計(jì)算年份,如果某天屬于新一年的第一個(gè)完整星期,則會(huì)歸為新年的星期年。
例如,如果某年的最后幾天在下一年開(kāi)始的第一個(gè)星期中,它們可能會(huì)被歸入下一年的 week year
。同理,如果新年的前幾天在上一年的最后一個(gè)完整星期內(nèi),這些天的星期年可能會(huì)歸屬上一年。這在日期和時(shí)間處理中可能導(dǎo)致意外的年份差異。
SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd");
System.out.println(sdf.format(new Date())); // 可能輸出與實(shí)際年份不同的值
Y
的潛在問(wèn)題
使用 Y
表示年份會(huì)引發(fā)一些日期計(jì)算的錯(cuò)誤,因?yàn)樗蕾囉谥軘?shù)的計(jì)算方式,不是每次都與實(shí)際的公歷年份一致。例如:
2025
的 week year
,導(dǎo)致使用 YYYY
格式化時(shí)得到 2025-12-31
。Y
表示年份可能會(huì)出現(xiàn)錯(cuò)誤,因?yàn)?week year
與通常理解的日歷年并不總是相符。Y
Y
一般僅用于需要符合 ISO 8601 標(biāo)準(zhǔn)的日期格式,特別是包含 ISO 周數(shù)(如“2024-W01-1”表示2024年的第一個(gè)星期一)的情況,而在一般情況下,我們都應(yīng)使用 y
來(lái)表示日歷年份。
y
來(lái)表示常規(guī)年份,避免日期格式化錯(cuò)誤。Y
來(lái)表示年份,除非確實(shí)需要按照 ISO 周年的格式來(lái)解析和顯示年份。在使用三目運(yùn)算符時(shí),類型對(duì)齊非常重要,因?yàn)槿窟\(yùn)算符的兩個(gè)分支會(huì)被類型推斷成一個(gè)共同的類型。若兩者類型不同,Java 編譯器會(huì)進(jìn)行類型提升或自動(dòng)轉(zhuǎn)換,這可能導(dǎo)致意外的類型變化和潛在的錯(cuò)誤。以下是需要注意的原因和細(xì)節(jié):
三目運(yùn)算符的返回值類型是根據(jù) true
和 false
分支的類型推斷出來(lái)的。為了得到一致的結(jié)果,Java 會(huì)自動(dòng)將不同的類型提升為更高精度的類型。例如,若一個(gè)分支返回 int
而另一個(gè)分支返回 double
,Java 會(huì)將 int
提升為 double
:
int x = 5;
double y = 10.5;
double result = (x > 0) ? x : y; // 返回 double 類型
System.out.println(result); // 輸出 5.0
這里返回值 5
被提升為 5.0
。雖然代碼在這個(gè)例子中不會(huì)出錯(cuò),但在某些情況下,這種自動(dòng)提升會(huì)導(dǎo)致意外的精度損失或類型不匹配的問(wèn)題。
NullPointerException
在 Java 中,基本類型和包裝類型的對(duì)齊需要特別小心。三目運(yùn)算符會(huì)嘗試將包裝類型和基本類型對(duì)齊成相同類型,這會(huì)導(dǎo)致自動(dòng)裝箱和拆箱,如果某個(gè)分支為 null
且需要拆箱,可能會(huì)引發(fā) NullPointerException
:
Integer a = null;
int b = 10;
int result = (a != null) ? a : b; // 如果 a 為 null,結(jié)果會(huì)發(fā)生自動(dòng)拆箱,引發(fā) NullPointerException
由于 a
為 null
,Java 會(huì)嘗試將其拆箱為 int
,從而拋出 NullPointerException
。為避免這種情況,可以確保類型對(duì)齊,或避免對(duì)可能為 null
的對(duì)象進(jìn)行拆箱。
如果三目運(yùn)算符的兩種返回類型無(wú)法被編譯器自動(dòng)轉(zhuǎn)換為一個(gè)兼容類型,代碼會(huì)直接報(bào)錯(cuò)。例如:
int x = 5;
String y = "10";
Object result = (x > 0) ? x : y; // 編譯錯(cuò)誤:int 和 String 不兼容
在這種情況下,int
和 String
無(wú)法被提升到相同類型,因此會(huì)引發(fā)編譯錯(cuò)誤。若確實(shí)希望返回不同類型的值,可以手動(dòng)指定共同的超類型,例如將結(jié)果定義為 Object
類型:
Object result = (x > 0) ? Integer.valueOf(x) : y; // 這里 result 為 Object
保持三目運(yùn)算符返回的類型一致,能讓代碼更加清晰,便于理解和維護(hù)。類型對(duì)齊可以避免類型轉(zhuǎn)換和自動(dòng)提升帶來(lái)的混亂,使代碼更容易預(yù)測(cè)和理解:
double result = (condition) ? 1.0 : 0.0; // 返回 double
true
和 false
分支的類型相同,避免意外的類型提升。null
參與三目運(yùn)算符計(jì)算。Object
或顯式轉(zhuǎn)換。初始化 HashMap
的容量大小是為了提高性能和減少內(nèi)存浪費(fèi)。通過(guò)設(shè)置合適的初始容量,可以減少 HashMap
的擴(kuò)容次數(shù),提高程序運(yùn)行效率。以下是詳細(xì)原因和建議:
HashMap
默認(rèn)的初始容量為 16,當(dāng)超過(guò)負(fù)載因子閾值(默認(rèn)是 0.75,即達(dá)到容量的 75%)時(shí),HashMap
會(huì)自動(dòng)進(jìn)行擴(kuò)容操作,將容量擴(kuò)大為原來(lái)的兩倍。擴(kuò)容涉及到重新計(jì)算哈希并將數(shù)據(jù)重新分布到新的桶中,這個(gè)過(guò)程非常耗時(shí),尤其在元素較多時(shí),擴(kuò)容會(huì)顯著影響性能。
通過(guò)設(shè)置合適的初始容量,可以避免或減少擴(kuò)容操作,提高 HashMap
的存取效率。
如果預(yù)計(jì)要存儲(chǔ)大量數(shù)據(jù)但沒(méi)有指定容量,HashMap
可能會(huì)多次擴(kuò)容,每次擴(kuò)容會(huì)分配新的內(nèi)存空間,并將原有數(shù)據(jù)復(fù)制到新空間中,造成內(nèi)存浪費(fèi)。如果在創(chuàng)建 HashMap
時(shí)能合理估算其容量,則可以一次性分配足夠的空間,從而避免重復(fù)分配內(nèi)存帶來(lái)的資源浪費(fèi)。
在并發(fā)環(huán)境下,頻繁擴(kuò)容可能導(dǎo)致線程不安全,即使是 ConcurrentHashMap
也不能完全避免擴(kuò)容帶來(lái)的性能和一致性問(wèn)題。初始化合適的容量可以減少并發(fā)環(huán)境下擴(kuò)容帶來(lái)的風(fēng)險(xiǎn)。
HashMap
將存儲(chǔ) n
個(gè)元素,可以將初始容量設(shè)置為 (n / 0.75)
,再向上取整為最接近的 2 的冪次方。 int initialCapacity = (int) Math.ceil(n / 0.75);
Map<String, String> map = new HashMap<>(initialCapacity);
HashMap
的容量總是以 2 的冪次方增長(zhǎng),因?yàn)樵谶M(jìn)行哈希運(yùn)算時(shí),可以高效利用按位與操作來(lái)計(jì)算哈希桶索引。因此,初始容量設(shè)為 2 的冪次方會(huì)使哈希分布更均勻。int expectedSize = 1000; // 預(yù)估需要存儲(chǔ)的鍵值對(duì)數(shù)量
int initialCapacity = (int) Math.ceil(expectedSize / 0.75);
HashMap<String, Integer> map = new HashMap<>(initialCapacity);
初始化 HashMap
的容量大小有以下好處:
合理初始化 HashMap
容量對(duì)于高性能應(yīng)用尤為重要,尤其在存儲(chǔ)大量數(shù)據(jù)時(shí)可以顯著提升程序的運(yùn)行效率。
在 Java 中創(chuàng)建線程池時(shí),不推薦直接使用 Executors
提供的快捷方法(例如 Executors.newFixedThreadPool()
、Executors.newCachedThreadPool()
等),而推薦使用 ThreadPoolExecutor
構(gòu)造方法來(lái)手動(dòng)配置線程池。這種做法主要是為了避免 Executors
創(chuàng)建線程池時(shí)隱藏的風(fēng)險(xiǎn),確保線程池配置符合需求。具體原因如下:
newFixedThreadPool()
和 newSingleThreadExecutor()
使用的是無(wú)界隊(duì)列 LinkedBlockingQueue
。無(wú)界隊(duì)列可以存放無(wú)限數(shù)量的任務(wù),一旦任務(wù)量非常大,隊(duì)列會(huì)迅速占用大量?jī)?nèi)存,導(dǎo)致 OutOfMemoryError(OOM)。
newCachedThreadPool()
使用的是 SynchronousQueue,該隊(duì)列沒(méi)有存儲(chǔ)任務(wù)的能力,每個(gè)任務(wù)到來(lái)時(shí)必須立即有一個(gè)空閑線程來(lái)處理任務(wù),否則將創(chuàng)建一個(gè)新線程。當(dāng)任務(wù)到達(dá)速度超過(guò)線程銷毀速度時(shí),線程數(shù)量會(huì)快速增加,導(dǎo)致 OOM。
在 newCachedThreadPool()
創(chuàng)建的線程池中,線程數(shù)沒(méi)有上限,短時(shí)間內(nèi)大量請(qǐng)求會(huì)導(dǎo)致線程數(shù)暴增,耗盡系統(tǒng)資源。newFixedThreadPool()
和 newSingleThreadExecutor()
雖然限制了核心線程數(shù),但未限制任務(wù)隊(duì)列長(zhǎng)度,依然可能耗盡內(nèi)存。
在業(yè)務(wù)需求不確定或任務(wù)激增的場(chǎng)景下,建議明確限制線程池的最大線程數(shù)和隊(duì)列長(zhǎng)度,以更好地控制系統(tǒng)資源的使用,避免因線程數(shù)無(wú)法控制導(dǎo)致的性能問(wèn)題。
Executors
創(chuàng)建的線程池默認(rèn)使用 AbortPolicy
拒絕策略,即當(dāng)線程池達(dá)到飽和時(shí)會(huì)拋出 RejectedExecutionException
異常。CallerRunsPolicy
(讓提交任務(wù)的線程執(zhí)行任務(wù))或 DiscardOldestPolicy
(丟棄最舊的任務(wù))來(lái)平衡任務(wù)處理。
手動(dòng)創(chuàng)建 ThreadPoolExecutor
時(shí),可以指定適合業(yè)務(wù)需求的拒絕策略,從而更靈活地處理線程池滿載的情況,避免異?;蛳到y(tǒng)性能下降。
使用 ThreadPoolExecutor
的構(gòu)造方法可以手動(dòng)設(shè)置以下參數(shù),以便根據(jù)業(yè)務(wù)需求靈活配置線程池:
這些參數(shù)的合理配置可以有效平衡線程池的性能、資源占用和任務(wù)處理能力,避免使用默認(rèn)配置時(shí)不符合需求的情況。
建議直接使用 ThreadPoolExecutor
構(gòu)造方法配置線程池,例如:
int corePoolSize = 10;
int maximumPoolSize = 20;
long keepAliveTime = 60L;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
workQueue,
new ThreadPoolExecutor.CallerRunsPolicy() // 拒絕策略
);
使用 Executors
創(chuàng)建線程池會(huì)帶來(lái)不易察覺(jué)的風(fēng)險(xiǎn),可能導(dǎo)致系統(tǒng)資源耗盡或任務(wù)堆積,手動(dòng)配置 ThreadPoolExecutor
可以更好地控制線程池的行為,使其符合實(shí)際業(yè)務(wù)需求和資源限制。因此,為了系統(tǒng)的健壯性和可控性,建議避免使用 Executors
快捷方法來(lái)創(chuàng)建線程池。
在使用 ArrayList
的 subList
方法時(shí)需要謹(jǐn)慎,因?yàn)樗幸恍撛诘南葳澹菀讓?dǎo)致意外的錯(cuò)誤和難以排查的異常。以下是 subList
需要小心使用的原因和注意事項(xiàng):
subList
返回的是視圖,而不是獨(dú)立副本
ArrayList
的 subList
方法返回的是原列表的一部分視圖(view
),而不是一個(gè)獨(dú)立的副本。對(duì) subList
的修改會(huì)直接影響原列表,反之亦然:
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
List<Integer> subList = list.subList(1, 4);
subList.set(0, 10); // 修改 subList
System.out.println(list); // 原列表也受到影響:[1, 10, 3, 4, 5]
這種共享視圖的機(jī)制在某些場(chǎng)景中可能引發(fā)意外的修改,導(dǎo)致數(shù)據(jù)被意外改變,從而影響到原始數(shù)據(jù)結(jié)構(gòu)的完整性和正確性。
subList
的結(jié)構(gòu)性修改限制
當(dāng)對(duì) ArrayList
本身(而非 subList
視圖)進(jìn)行結(jié)構(gòu)性修改(add
、remove
等改變列表大小的操作)后,再操作 subList
會(huì)導(dǎo)致 ConcurrentModificationException
異常。這是因?yàn)?subList
和原 ArrayList
之間共享結(jié)構(gòu)性修改的狀態(tài),一旦其中一個(gè)發(fā)生修改,另一方就會(huì)失效:
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
List<Integer> subList = list.subList(1, 4);
list.add(6); // 修改原列表的結(jié)構(gòu)
subList.get(0); // 拋出 ConcurrentModificationException
這種限制意味著 subList
不適合在列表頻繁變化的場(chǎng)景中使用,否則很容易引發(fā)并發(fā)修改異常。
subList
和 ArrayList
的 removeAll 等操作可能導(dǎo)致錯(cuò)誤
subList
生成的視圖列表可能會(huì)在批量刪除操作中出現(xiàn)問(wèn)題,例如調(diào)用 removeAll
方法時(shí),subList
的行為不一致或發(fā)生異常。對(duì)于 ArrayList
的 subList
,一些批量修改方法(如 removeAll
、retainAll
)可能會(huì)在刪除視圖元素后,導(dǎo)致 ArrayList
產(chǎn)生不可預(yù)料的狀態(tài),甚至引發(fā) IndexOutOfBoundsException
等異常。
如果需要一個(gè)獨(dú)立的子列表,可以通過(guò) new ArrayList<>(originalList.subList(start, end))
來(lái)創(chuàng)建一個(gè)子列表的副本,從而避免 subList
的共享視圖問(wèn)題:
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
ArrayList<Integer> subListCopy = new ArrayList<>(list.subList(1, 4)); // 創(chuàng)建副本
list.add(6); // 修改原列表
subListCopy.get(0); // 安全,不會(huì)受到影響
使用 ArrayList
的 subList
方法需要注意以下幾點(diǎn):
subList
只是原列表的視圖,修改其中一個(gè)會(huì)影響另一個(gè)。subList
會(huì)拋出 ConcurrentModificationException
。subList
的批量操作可能引發(fā)不可預(yù)料的錯(cuò)誤。subList
的副本以避免潛在問(wèn)題。
謹(jǐn)慎使用 subList
可以避免意外的錯(cuò)誤,提高代碼的健壯性。
在 Java 中,禁止在 foreach
循環(huán)中進(jìn)行元素的 remove
或 add
操作,主要是因?yàn)檫@種操作可能導(dǎo)致 ConcurrentModificationException
異常,或者導(dǎo)致循環(huán)行為不符合預(yù)期。具體原因如下:
ConcurrentModificationException
異常
當(dāng)你在 foreach
循環(huán)中直接修改集合(例如 remove
或 add
元素),會(huì)導(dǎo)致并發(fā)修改問(wèn)題。foreach
循環(huán)底層使用了集合的 Iterator
來(lái)遍歷元素。大多數(shù)集合類(如 ArrayList
、HashSet
等)都會(huì)維護(hù)一個(gè) modCount
計(jì)數(shù)器,表示集合的結(jié)構(gòu)變更次數(shù)。當(dāng)你在遍歷時(shí)修改集合的結(jié)構(gòu)(如刪除或添加元素),modCount
會(huì)發(fā)生變化,而 Iterator
會(huì)檢測(cè)到這種結(jié)構(gòu)性修改,從而拋出 ConcurrentModificationException
異常,防止程序在多線程環(huán)境中出現(xiàn)意外行為。
例如:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (String s : list) {
if (s.equals("b")) {
list.remove(s); // 會(huì)拋出 ConcurrentModificationException
}
}
在上面的代碼中,foreach
循環(huán)遍歷 list
時(shí),如果刪除了元素 b
,它會(huì)修改 list
的結(jié)構(gòu),從而導(dǎo)致 Iterator
檢測(cè)到并發(fā)修改,拋出異常。
即使沒(méi)有拋出 ConcurrentModificationException
,在 foreach
循環(huán)中修改集合也會(huì)導(dǎo)致不可預(yù)測(cè)的行為。例如,remove
或 add
操作會(huì)改變集合的大小和內(nèi)容,可能會(huì)影響迭代的順序或?qū)е逻z漏某些元素,甚至造成死循環(huán)或跳過(guò)某些元素。
例如:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (String s : list) {
if (s.equals("b")) {
list.add("e"); // 修改集合的大小
}
System.out.println(s);
}
在這個(gè)例子中,add
操作會(huì)向 list
中添加一個(gè)新元素 "e"
,從而修改了集合的結(jié)構(gòu)。因?yàn)?foreach
循環(huán)的內(nèi)部實(shí)現(xiàn)使用了迭代器,它可能不會(huì)考慮到修改后的新元素,導(dǎo)致輸出順序或遍歷結(jié)果與預(yù)期不同。
remove()
方法
如果需要在循環(huán)中刪除元素,推薦使用 Iterator
顯式地進(jìn)行刪除操作。Iterator
提供了一個(gè)安全的 remove()
方法,可以在遍歷時(shí)安全地刪除元素,而不會(huì)引發(fā) ConcurrentModificationException
。
例如:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if (s.equals("b")) {
iterator.remove(); // 使用 Iterator 的 remove() 方法
}
}
使用 Iterator.remove()
可以安全地在遍歷時(shí)刪除元素,而不會(huì)拋出并發(fā)修改異常。
在 foreach
循環(huán)中直接進(jìn)行 remove
或 add
操作是不安全的,主要有以下原因:
ConcurrentModificationException
:直接修改集合會(huì)觸發(fā)迭代器的并發(fā)修改檢測(cè),導(dǎo)致異常。Iterator
替代:使用 Iterator
的 remove()
方法可以避免這些問(wèn)題,實(shí)現(xiàn)安全的元素刪除操作。
因此,正確的做法是使用 Iterator
顯式地處理元素的刪除或修改,而不是直接在 foreach
循環(huán)中進(jìn)行修改。
在很多工程實(shí)踐中,禁止工程師直接使用日志系統(tǒng)(如 Log4j、Logback)中的 API,主要是出于以下幾個(gè)原因:
直接使用日志系統(tǒng)的 API 可能會(huì)導(dǎo)致日志記錄邏輯與應(yīng)用的業(yè)務(wù)邏輯緊密耦合,使得日志配置和實(shí)現(xiàn)的分離變得困難?,F(xiàn)代的日志框架(如 Log4j、Logback)允許通過(guò)外部配置文件(如 log4j.xml
或 logback.xml
)靈活配置日志級(jí)別、輸出格式、輸出位置等,而不是硬編碼到應(yīng)用代碼中。直接使用日志 API 會(huì)導(dǎo)致日志的配置與業(yè)務(wù)代碼綁定在一起,不易修改和維護(hù)。
建議的做法:通過(guò)使用日志框架的日志抽象接口(如 org.slf4j.Logger
)來(lái)記錄日志,而不是直接依賴具體的日志實(shí)現(xiàn)。這種方式提供了更大的靈活性,日志實(shí)現(xiàn)可以在運(yùn)行時(shí)通過(guò)配置文件更換而無(wú)需修改代碼。
如果工程師直接使用日志庫(kù)的 API,項(xiàng)目在需要切換日志框架(比如從 Log4j 轉(zhuǎn)換到 Logback 或其他框架)時(shí),需要修改大量的代碼,增加了系統(tǒng)的耦合度和維護(hù)難度。另一方面,使用日志抽象層(如 SLF4J)可以避免這一問(wèn)題,因?yàn)?SLF4J 是一個(gè)日志抽象層,底層可以切換具體的日志實(shí)現(xiàn)而無(wú)需改變業(yè)務(wù)代碼。
示例:
// 不推薦:直接使用 Log4j 的 API
import org.apache.log4j.Logger;
Logger logger = Logger.getLogger(MyClass.class);
logger.info("This is a log message");
// 推薦:通過(guò) SLF4J 接口來(lái)記錄日志
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Logger logger = LoggerFactory.getLogger(MyClass.class);
logger.info("This is a log message");
使用 SLF4J 可以在不同的環(huán)境中靈活切換日志實(shí)現(xiàn),而無(wú)需修改代碼。
如果工程師直接使用日志框架的 API,可能會(huì)在日志記錄時(shí)不遵循一致的日志策略。例如,日志的級(jí)別、格式、日志輸出的內(nèi)容等可能不統(tǒng)一,導(dǎo)致日志信息混亂、不易追蹤。通過(guò)統(tǒng)一的日志抽象接口(如 SLF4J)和規(guī)范的日志記錄策略(通過(guò) AOP 或日志框架自帶的特性)可以保持日志的一致性和規(guī)范性。
最佳實(shí)踐:
DEBUG
、INFO
、WARN
、ERROR
)和標(biāo)準(zhǔn)格式。日志記錄可能對(duì)應(yīng)用的性能產(chǎn)生一定的影響,尤其是在日志記錄過(guò)于頻繁或日志輸出內(nèi)容過(guò)多的情況下。通過(guò)直接使用日志框架的 API,可能無(wú)法靈活控制日志輸出的頻率、內(nèi)容或過(guò)濾策略,從而造成性能問(wèn)題。很多日志框架(如 Log4j 和 Logback)提供了高級(jí)的配置選項(xiàng),如異步日志、日志緩存等特性,可以顯著提高性能。
推薦做法:
在團(tuán)隊(duì)開(kāi)發(fā)中,直接使用日志框架的 API 會(huì)導(dǎo)致不同開(kāi)發(fā)人員在不同模塊中記錄日志時(shí)不遵循統(tǒng)一規(guī)范,導(dǎo)致日志格式不統(tǒng)一、信息不一致,甚至產(chǎn)生重復(fù)的日志記錄。通過(guò)日志管理工具類或封裝類,可以確保所有開(kāi)發(fā)人員遵循統(tǒng)一的日志記錄策略。
示例:
LoggerFactory
工廠類來(lái)生成日志記錄對(duì)象。禁止工程師直接使用日志系統(tǒng)(如 Log4j、Logback)中的 API,主要是為了:
最好的做法是通過(guò)日志抽象層(如 SLF4J)進(jìn)行日志記錄,同時(shí)通過(guò)日志管理工具類進(jìn)行統(tǒng)一的配置和調(diào)用,確保日志的高效、規(guī)范和靈活性。
在面向?qū)ο缶幊蹋∣OP)中,繼承是一種常見(jiàn)的代碼復(fù)用方式,它允許一個(gè)類繼承另一個(gè)類的屬性和行為。然而,雖然繼承可以提高代碼的復(fù)用性,但過(guò)度或不當(dāng)使用繼承可能會(huì)導(dǎo)致代碼的復(fù)雜性增加,進(jìn)而帶來(lái)一些潛在的問(wèn)題。因此,建議開(kāi)發(fā)者在使用繼承時(shí)要謹(jǐn)慎,以下是一些關(guān)鍵原因:
繼承會(huì)導(dǎo)致子類和父類之間形成緊密的耦合關(guān)系。子類依賴于父類的實(shí)現(xiàn),這意味著如果父類發(fā)生變化,可能會(huì)影響到所有繼承自該父類的子類,導(dǎo)致修改和維護(hù)變得更加困難。這種緊密耦合關(guān)系也限制了子類的靈活性,因?yàn)樗仨氉裱割惖慕涌诤蛯?shí)現(xiàn)。
例子:
class Animal {
void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
@Override
void eat() {
System.out.println("Dog is eating");
}
}
如果父類 Animal
做了改動(dòng)(如修改 eat()
方法的實(shí)現(xiàn)),Dog
類也會(huì)受到影響。這樣的耦合會(huì)增加后期維護(hù)的復(fù)雜度。
繼承可能破壞封裝性,因?yàn)樽宇惪梢灾苯釉L問(wèn)父類的成員(字段和方法),尤其是當(dāng)父類成員被設(shè)置為 protected
或 public
時(shí)。這種情況可能導(dǎo)致子類暴露不應(yīng)被外界訪問(wèn)的細(xì)節(jié),破壞了數(shù)據(jù)的封裝性。
例子:
class Vehicle {
protected int speed;
}
class Car extends Vehicle {
void accelerate() {
speed += 10; // 直接訪問(wèn)父類的 protected 字段
}
}
在這種情況下,Car
類直接訪問(wèn)了父類 Vehicle
的 speed
字段,而不是通過(guò)公共接口來(lái)修改它,導(dǎo)致封裝性降低。
繼承往往會(huì)導(dǎo)致不合理的類層次結(jié)構(gòu),特別是在試圖通過(guò)繼承來(lái)表達(dá)“是一個(gè)”(is-a
)關(guān)系時(shí),實(shí)際情況可能并不符合這種邏輯。濫用繼承可能會(huì)使類之間的關(guān)系變得復(fù)雜和不直觀,導(dǎo)致代碼結(jié)構(gòu)混亂。
例子:
假設(shè)我們有一個(gè) Car
類和一個(gè) Truck
類,都繼承自 Vehicle
類。如果 Car
和 Truck
共享很多方法和屬性,這樣的設(shè)計(jì)可能是合適的。但是,如果 Car
和 Truck
之間差異很大,僅通過(guò)繼承來(lái)構(gòu)建它們的關(guān)系,可能會(huì)導(dǎo)致繼承層次過(guò)于復(fù)雜,代碼閱讀和理解變得困難。
由于子類繼承了父類的行為,任何對(duì)父類的修改都有可能影響到子類的行為。更糟糕的是,錯(cuò)誤或不一致的修改可能在父類中發(fā)生,而這些錯(cuò)誤可能不會(huì)立即暴露出來(lái),直到程序運(yùn)行到某個(gè)特定的地方,才會(huì)顯現(xiàn)出錯(cuò)誤。
例子: 假設(shè)你修改了父類的某個(gè)方法,但忘記更新或調(diào)整子類中相應(yīng)的重寫方法,這可能會(huì)導(dǎo)致難以發(fā)現(xiàn)的錯(cuò)誤。
繼承創(chuàng)建了一個(gè)父類與子類之間的固定關(guān)系,這意味著如果你想在一個(gè)完全不同的上下文中重用一個(gè)類,你可能不能通過(guò)繼承來(lái)實(shí)現(xiàn)。在某些情況下,組合比繼承更為靈活,允許你將多個(gè)行為組合到一個(gè)類中,而不是通過(guò)繼承來(lái)強(qiáng)行構(gòu)建類的層次結(jié)構(gòu)。
例子:
// 組合而非繼承
class Engine {
void start() {
System.out.println("Engine started");
}
}
class Car {
private Engine engine = new Engine(); // 通過(guò)組合來(lái)使用 Engine
void start() {
engine.start();
}
}
通過(guò)組合,可以靈活地使用不同的組件,而不需要繼承整個(gè)類。這樣做的優(yōu)點(diǎn)是更具擴(kuò)展性和靈活性。
如果你過(guò)度依賴?yán)^承,你的代碼會(huì)容易受到父類實(shí)現(xiàn)的限制,難以靈活地添加新功能或進(jìn)行擴(kuò)展。例如,在繼承鏈中添加新的功能可能會(huì)導(dǎo)致一大堆方法的修改和重寫,而不通過(guò)繼承,可以更輕松地將功能作為獨(dú)立模塊來(lái)重用。
相比繼承,接口(Interface) 和 組合(Composition) 更符合面向?qū)ο笤O(shè)計(jì)的原則。接口允許類只暴露所需的功能,而不暴露實(shí)現(xiàn)細(xì)節(jié),組合則允許你將多個(gè)不同的行為組合在一起,使得系統(tǒng)更加靈活和可擴(kuò)展。通過(guò)接口和組合,可以避免繼承的許多問(wèn)題。
推薦設(shè)計(jì)模式:
盡管繼承是面向?qū)ο缶幊讨械囊粋€(gè)重要特性,但濫用繼承可能帶來(lái)許多問(wèn)題,特別是在以下幾個(gè)方面:
因此,推薦優(yōu)先使用組合而非繼承,并盡可能使用接口來(lái)實(shí)現(xiàn)靈活的擴(kuò)展。如果必須使用繼承,確保它能夠清晰地表達(dá)“是一個(gè)”的關(guān)系,并避免過(guò)深的繼承層次。
serialVersionUID
是 Java 中用來(lái)標(biāo)識(shí)序列化版本的一個(gè)靜態(tài)字段。它的作用是確保在反序列化時(shí),JVM 可以驗(yàn)證序列化的類與當(dāng)前類的兼容性,以避免版本不兼容導(dǎo)致的錯(cuò)誤。盡管 serialVersionUID
可以由開(kāi)發(fā)人員手動(dòng)定義,禁止開(kāi)發(fā)人員修改 serialVersionUID
字段的值 的原因如下:
serialVersionUID
的主要作用是保證在序列化和反序列化過(guò)程中,類的版本兼容性。它是用來(lái)標(biāo)識(shí)類的版本的,如果序列化和反序列化過(guò)程中使用的類的 serialVersionUID
不匹配,就會(huì)拋出 InvalidClassException
。
serialVersionUID
會(huì)導(dǎo)致序列化的數(shù)據(jù)與當(dāng)前類不兼容,導(dǎo)致反序列化失敗。serialVersionUID
的值會(huì)改變類的版本標(biāo)識(shí),導(dǎo)致已序列化的數(shù)據(jù)在反序列化時(shí)不能成功讀取,特別是在類結(jié)構(gòu)發(fā)生改變(例如添加或刪除字段)時(shí)。例如:
// 類的第一次版本
public class MyClass implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
// 其他字段和方法
}
// 類的第二次修改版本
public class MyClass implements Serializable {
private static final long serialVersionUID = 2L; // 修改了 serialVersionUID
private String name;
private int age; // 新增字段
// 其他字段和方法
}
如果修改了 serialVersionUID
,而之前序列化的數(shù)據(jù)是使用版本 1 的類進(jìn)行序列化的,反序列化時(shí)會(huì)因?yàn)?serialVersionUID
不匹配而導(dǎo)致失敗。
Java 會(huì)根據(jù)類的字段、方法等信息自動(dòng)生成 serialVersionUID
,這個(gè)值是基于類的結(jié)構(gòu)計(jì)算出來(lái)的。如果開(kāi)發(fā)人員修改了 serialVersionUID
,可能會(huì)破壞 Java 自動(dòng)生成的版本控制機(jī)制,從而導(dǎo)致版本控制不一致,增加了維護(hù)復(fù)雜性。
如果手動(dòng)修改 serialVersionUID
,容易出現(xiàn)以下幾種問(wèn)題:
serialVersionUID
可能會(huì)導(dǎo)致已序列化的數(shù)據(jù)無(wú)法恢復(fù)。serialVersionUID
,可能會(huì)在不同的機(jī)器或系統(tǒng)間引起序列化不一致。Java 提供了兩種主要的兼容性規(guī)則:
serialVersionUID
,則反序列化是可以工作的。serialVersionUID
,反序列化仍然可以工作。
如果不小心修改了 serialVersionUID
,可能導(dǎo)致以下情況:
serialVersionUID
:Java 會(huì)根據(jù)類的結(jié)構(gòu)自動(dòng)生成 serialVersionUID
,這樣如果類的結(jié)構(gòu)發(fā)生變化,serialVersionUID
會(huì)自動(dòng)變化,確保不兼容的版本之間不會(huì)出現(xiàn)意外的反序列化行為。serialVersionUID
:手動(dòng)修改 serialVersionUID
可能導(dǎo)致版本控制不一致,特別是在多人開(kāi)發(fā)、分布式部署的環(huán)境中,容易出現(xiàn)反序列化失敗的問(wèn)題。
手動(dòng)修改 serialVersionUID
可能會(huì)導(dǎo)致數(shù)據(jù)丟失或反序列化時(shí)拋出異常。例如,如果開(kāi)發(fā)人員錯(cuò)誤地修改了 serialVersionUID
,系統(tǒng)在嘗試反序列化時(shí)可能會(huì)因?yàn)?serialVersionUID
不匹配而無(wú)法成功加載對(duì)象,導(dǎo)致異常的發(fā)生。
禁止開(kāi)發(fā)人員修改 serialVersionUID
字段的值,主要是為了:
serialVersionUID
的優(yōu)勢(shì),保證類的版本一致性和可維護(hù)性。
如果確實(shí)需要修改 serialVersionUID
,應(yīng)確保修改后的版本與已經(jīng)序列化的數(shù)據(jù)兼容,并遵循合理的版本管理策略。
禁止開(kāi)發(fā)人員使用 isSuccess
作為變量名,主要是為了遵循更好的編程規(guī)范和提高代碼的可讀性、可維護(hù)性。這個(gè)變量名問(wèn)題的核心在于其容易引起歧義和混淆。具體原因如下:
在 Java 中,通常使用 is
或 has
開(kāi)頭的變量名來(lái)表示布爾值(boolean
類型)。這類命名通常遵循特定的語(yǔ)義約定,表示某個(gè)條件是否成立。例如:
isEnabled
表示某個(gè)功能是否啟用;hasPermission
表示是否有權(quán)限。問(wèn)題:
isSuccess
看起來(lái)像一個(gè)布爾值(boolean
類型),但它實(shí)際上可能并不直接表示一個(gè)布爾值,而是一個(gè)狀態(tài)或結(jié)果。這種命名可能會(huì)導(dǎo)致混淆,開(kāi)發(fā)者可能誤以為它是布爾類型的變量,而實(shí)際上它可能是一個(gè)描述狀態(tài)的對(duì)象、字符串或者其他類型的數(shù)據(jù)。
isSuccess
這個(gè)名字表面上表示“是否成功”,但是它缺少具體的上下文,導(dǎo)致語(yǔ)義不夠明確。真正表示是否成功的布爾值應(yīng)該直接使用 boolean
類型的變量,并且使用清晰明確的命名。
例如:
isCompleted
:表示某個(gè)任務(wù)是否完成。isSuccessful
:表示某個(gè)操作是否成功。這些命名能更明確地表達(dá)布爾變量的含義,避免理解上的歧義。
is
前綴混淆
is
前綴通常用來(lái)表示“是否”某個(gè)條件成立,適用于返回布爾值的方法或者變量。isSuccess
這樣的命名會(huì)讓開(kāi)發(fā)人員誤以為它是一個(gè)布爾值,或者一個(gè) boolean
類型的值,但實(shí)際上它可能是一個(gè)復(fù)雜類型或者其他非布爾類型,造成不必要的混淆。
例如:
boolean isSuccess = someMethod(); // 看起來(lái)是布爾值,但實(shí)際類型可能不同
這種情況可能導(dǎo)致開(kāi)發(fā)人員產(chǎn)生誤解,認(rèn)為 isSuccess
代表的是布爾值,但它可能是某個(gè)表示成功的對(duì)象、枚舉或者其他數(shù)據(jù)類型。
為了避免歧義和混淆,開(kāi)發(fā)人員應(yīng)使用更加明確且符合命名規(guī)范的名稱。以下是一些命名的改進(jìn)建議:
isSuccessful
或 wasSuccessful
。operationResult
或 statusCode
,以表明它是一個(gè)描述操作結(jié)果的變量。
清晰且具有意義的命名能夠幫助團(tuán)隊(duì)成員或未來(lái)的開(kāi)發(fā)者更快地理解代碼的意圖。如果變量名過(guò)于模糊(如 isSuccess
),就可能讓人對(duì)其實(shí)際含義產(chǎn)生疑問(wèn),尤其是在閱讀較大或復(fù)雜的代碼時(shí)。良好的命名能夠提升代碼的可讀性和可維護(hù)性。
isSuccess
這樣的命名不清晰,容易與布爾類型的變量產(chǎn)生混淆,進(jìn)而影響代碼的可讀性。isSuccessful
或 wasSuccessful
,更清晰地表達(dá)變量的意義。以上是 V 哥精心總結(jié)的13個(gè) Java 編程中的小小編碼問(wèn)題,也是V 哥日常編碼中總結(jié)的學(xué)習(xí)筆記,分享給大家,如果內(nèi)容對(duì)你有幫助,請(qǐng)不要吝嗇來(lái)個(gè)小贊唄,關(guān)注威哥愛(ài)編程,Java 路上,你我相伴前行。Java編程中的13個(gè)靈魂拷問(wèn):最佳實(shí)踐與常見(jiàn)錯(cuò)誤避坑指南
更多建議: