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