摘要
這篇文章是關于Java開發中阿里巴巴編碼規范的經驗總結。它強調了避免使用Apache BeanUtils進行屬性復制,因為它效率低下且類型轉換不安全。推薦使用Spring BeanUtils、Hutool BeanUtil、MapStruct或手動賦值等替代方案。文章還指出不應在視圖模板中加入復雜邏輯運算,應明確MVC架構各層的職責。此外,還涉及數據結構初始化應指定大小、正則表達式的預編譯、避免通過catch處理某些RuntimeException異常、finally塊中資源關閉的正確方式以及防止NPE的多種方法。
1. 【強制】避免用 ApacheBeanutils 進行屬性的 copy。
不推薦使用 Apache Commons BeanUtils 工具來進行對象屬性復制(如 BeanUtils.copyProperties),因為它效率低、性能差、類型轉換不安全,在生產環境中容易成為性能瓶頸。
Apache BeanUtils 是通過反射 + 內省(Introspector)+ 字符串轉換來做屬性 copy,性能非常低,不適合在高并發或大量對象轉換場景中使用。
1.1. 屬性賦值推薦方案
方案 | 優勢 | 場景 |
Spring BeanUtils | 性能略優于 Apache,但仍是反射 | 適合小量級對象拷貝 |
Hutool BeanUtil | 性能高,支持深拷貝、自定義字段映射 | 推薦在工具類中統一封裝 |
MapStruct | 編譯期生成拷貝代碼(無反射,極快) | 推薦在 DDD 中的 DO <-> DTO 映射 |
手動賦值 | 最安全、最清晰 | 小對象或關鍵轉換邏輯 |
ModelMapper / Dozer(不推薦) | 仍是反射,配置復雜,性能低 | 不推薦使用 |
2. 【強制】不要在視圖模板中加入任何復雜的邏輯運算。
在 MVC 架構的具體實現中,比如 Spring Boot 項目中,我們常見的結構包括:
- Controller(控制器)
- Service(服務/業務邏輯層)
- DAO(數據訪問層,也叫 Mapper、Repository)
下面是這三層的職責和理解方式,結合“不要在視圖中寫復雜邏輯”的那條建議,進一步深化層次的劃分:
2.1. Controller:控制層
職責:
- 接收 HTTP 請求參數;
- 調用 Service 進行處理;
- 封裝和返回響應數據(
Response
); - 做參數校驗、權限判斷、日志記錄等外圍操作。
不要做的事:
- 不要寫業務邏輯;
- 不要操作數據庫;
- 不要做復雜的流程判斷或數據處理。
示例:
@PostMapping("/user/upgrade")
public Response<Void> upgradeUser(@RequestBody UserUpgradeRequest request) {userService.upgradeUserToVip(request.getUserId());return Response.success();
}
2.2. Service:業務邏輯層
職責:
- 實現具體業務邏輯,如“升級用戶為 VIP”、“扣減庫存”、“發送通知”等;
- 調用多個 DAO、封裝業務判斷流程;
- 做事務控制(
@Transactional
); - 組裝處理結果返回 Controller。
不要做的事:
- 不要和 Web 框架(如 Servlet、HttpRequest)耦合;
- 不要拼 SQL,不直接操作數據庫。
示例:
public void upgradeUserToVip(Long userId) {UserDO user = userDao.findById(userId);if (user == null || user.isVip()) {throw new BizException("用戶不存在或已是VIP");}user.setVip(true);userDao.update(user);notifyService.sendVipNotification(user);
}
2.3. DAO(Mapper/Repository):數據訪問層
職責:
- 直接與數據庫交互;
- 封裝 SQL 查詢(或通過 MyBatis/JPA 映射);
- 只做增刪改查操作;
- 返回實體對象,不做業務判斷。
不要做的事:
- 不要處理業務邏輯;
- 不要做流程判斷;
- 不要拼接復雜結果(如組裝響應對象)。
示例:
@Mapper
public interface UserDao {UserDO findById(Long userId);int update(UserDO user);
}
2.4. 總結類比:工廠分工
層級 | 類比 | 職責 |
Controller | 前臺接待 | 接收客戶請求,轉交到內部處理 |
Service | 經理 | 安排工人干活,處理流程,判斷異常 |
DAO | 工人 | 操作數據庫,搬原材料,不決策 |
2.5. MVC 和模板邏輯的對應關系
- 視圖(View) = 頁面模板、前端:只能展示數據,不參與 Controller、Service、DAO 的職責。
- 所以復雜判斷、業務數據準備都應該在 Service/Controller 中完成,模板中直接展示結果即可。
3. 【強制】任何數據結構的構造或初始化,都應指定大小,避免數據結構無限增長吃光內存。
3.1. 這條建議的核心思想是
在使用如集合(List、Map、Set 等)這類可擴展數據結構時,應盡可能“預估其大小”并“顯式設置初始容量”,從而避免它們在運行中頻繁擴容、內存抖動,甚至 OOM(內存溢出)的問題。提前預估數據量,合理初始化集合容量,是性能優化與內存安全的重要實踐。
ArrayList
HashMap
HashSet
ConcurrentHashMap
StringBuilder
自定義緩存、隊列等
這些類的背后都依賴一個數組或哈希桶來存儲數據,如果你沒有指定容量,它們會用默認大小初始化,然后在插入過程中自動擴容(重新開數組、拷貝數據等)。
3.2. 為什么要指定大小?
3.2.1. 不指定容量的風險:
- 頻繁擴容: 每次容量不夠都要重新分配數組,拷貝舊數據 ? 性能開銷大;
- 內存浪費: 擴容步長不是線性的,可能會分配遠超實際需要的空間;
- 內存溢出(OOM): 在循環里構造數據結構沒有設置上限 ? 無限增長,吃光堆內存。
3.2.2. 指定容量的好處:
- 減少擴容次數: 提高性能;
- 控制內存: 限制最大容量,防止意外 OOM;
- 體現程序邊界意識: 編碼更健壯。
3.3. 數據結構設置初始值示例對比
3.3.1. 不指定大小(有性能隱患):
List<String> list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {list.add("item" + i);
}
默認容量是 10,之后 1.5 倍擴容 ? 至少擴容 10+ 次,代價很高
3.3.2. 指定大小(性能友好):
List<String> list = new ArrayList<>(100000);
for (int i = 0; i < 100000; i++) {list.add("item" + i);
}
只創建一次內部數組,避免擴容
3.4. 延伸到場景
數據結構 | 默認容量 | 推薦用法 |
| 10 | new ArrayList<>(預計數量) |
| 16 | new HashMap<>(預計數量 / 負載因子 + 1) |
| 16 | new StringBuilder(預計字符串長度) |
| 16 | new ConcurrentHashMap<>(預計大小) |
4. 【強制】在使用正則表達式時,利用好其預編譯功能,可以有效加快正則匹配速度。
說明:不要在方法體內定義:Pattern pattern = Pattern.compile("規則");
正則表達式在使用時,如果每次都重新編譯,會嚴重影響性能。應該使用預編譯(Pattern.compile(...)
)方式,將正則表達式提前編譯好并重復使用。
在 Java 中使用正則時,一般有兩種方式:
4.1. 每次都編譯(效率低)
boolean isMatch = "abc123".matches("\\w+");
內部其實相當于:
Pattern.compile("\\w+").matcher("abc123").matches();
這會每次調用都重新編譯正則表達式,開銷很大,尤其在循環或高并發下。
4.2. 預編譯后復用(推薦)
private static final Pattern PATTERN = Pattern.compile("\\w+");boolean isMatch = PATTERN.matcher("abc123").matches();
正則表達式只在類加載時編譯一次,后續調用直接復用,提高性能。
4.3. 使用場景
場景 | 是否推薦預編譯 |
單次用、不頻繁 | 可以臨時用 |
多次校驗、循環中用 | 必須預編譯 |
高并發服務接口中 | 必須預編譯 |
工具類/公共方法 | 強烈建議預編譯并靜態緩存 |
4.4. 總結
- 編譯正則是耗時操作;
- 多次使用時,一定要用
Pattern.compile(...)
并緩存起來; - 正則預編譯 = 性能優化 + 好習慣。
5. 【強制】Java 類庫中定義的可以通過預檢查方式規避的 RuntimeException 異常不應該通過 catch 的方式來處理,比如:NullPointerException,IndexOutOfBoundsException 等等。
try-catch 是用來處理不可預知的異常情況,不是用來“代替 if 判斷”的。對于 Java 類庫中常見的 RuntimeException(運行時異常),如果我們可以在代碼運行前通過邏輯“預檢查”避免它的發生,就不應該依賴 try-catch 來處理它。
5.1. 舉幾個典型例子
5.1.1. 不推薦的做法(用 catch
捕獲 NPE):
try {System.out.println(user.getName());
} catch (NullPointerException e) {// 捕獲空指針異常System.out.println("user 為空");
}
5.1.2. 推薦的做法(用 if
判斷提前規避):
if (user != null) {System.out.println(user.getName());
} else {System.out.println("user 為空");
}
5.2. 為什么不推薦用 catch 處理這些異常?
- 這類異常不是業務異常,而是代碼邏輯錯誤:出現 NullPointer、數組越界等,說明你的代碼邏輯寫得有問題,不是正常的“可恢復”情況。
- catch 成本高,影響性能:try-catch 的異常捕獲機制在 JVM 中性能是開銷較大的(尤其是頻繁拋異常的情況)。
- 可讀性變差,調試困難:濫用 catch 會把真正的問題掩蓋,調試困難,也不利于代碼維護。
5.3. 適用的異常類型(不建議 catch)
異常類 | 說明 |
| 空指針異常,應通過非空判斷避免 |
| 下標越界,應判斷下標是否合法 |
| 類型轉換錯誤,應先 |
| 參數非法,應通過參數校驗處理 |
5.4. 異常捕獲正確的原則
- 能通過邏輯避免的異常,不要 try-catch
- RuntimeException 更多是一種編碼警告,不是業務流程的一部分
- 只在頂層兜底或做日志監控時統一捕獲這些異常
6. 【強制】finally 塊必須對資源對象、 流對象進行關閉,有異常也要做 try-catch。
無論是否發生異常,finally
塊中一定要確保資源被正確關閉,且關閉操作本身也要加 try-catch
,避免二次異常導致資源未釋放。
6.1. 正確的使用方式示例:
自 Java 7 起,Java 提供了 try-with-resources
語法,它能夠自動關閉實現了 AutoCloseable
或 Closeable
接口的資源(如 InputStream
)。使用該語法,可以消除手動管理資源關閉的復雜性,并自動處理 close()
方法可能拋出的異常。
try (InputStream in = new FileInputStream("data.txt")) {// 讀文件邏輯
} catch (IOException e) {e.printStackTrace(); // 異常處理
}
6.2. 錯誤的示例(不捕獲關閉異常):
finally {in.close(); // 如果這里拋出 IOException,整個異常流程會被覆蓋
}
6.3. 適用范圍:
這條規范適用于所有需要關閉或釋放的資源類,例如:
- IO 流(InputStream、OutputStream、Reader、Writer 等)
- 數據庫連接(Connection、Statement、ResultSet)
- 網絡資源(Socket、HttpURLConnection)
- 文件句柄
- 線程池(ExecutorService 的
shutdown
) - 鎖(
Lock.unlock()
)
7. 【推薦】防止 NPE,是程序員的基本修養,注意 NPE 產生的場景
1)返回類型為基本數據類型,return 包裝數據類型的對象時,自動拆箱有可能產生 NPE,反例:public int method() { return Integer 對象; },如果為 null,自動解箱拋 NPE。
2)數據庫的查詢結果可能為 null。
3)集合里的元素即使 isNotEmpty,取出的數據元素也可能為 null。
4)遠程調用返回對象時,一律要求進行空指針判斷,防止 NPE。
5)對于 Session 中獲取的數據,建議進行 NPE 檢查,避免空指針。
6)級聯調用 obj.getA().getB().getC();一連串調用,易產生 NPE。正例: 使用 JDK8 的 Optional 類來防止 NPE 問題。
7.1. 常見的 NPE 產生場景
7.1.1. 訪問未初始化的對象
當你嘗試訪問一個未初始化的對象(即其值為 null
)時,通常會拋出 NPE。
String str = null;
int length = str.length(); // NPE: str 是 null,無法調用 length()
7.1.2. 調用 null
對象的實例方法
如果對象為 null
,直接調用其方法會導致空指針異常。
MyClass obj = null;
obj.someMethod(); // NPE: obj 是 null,無法調用 someMethod()
7.1.3. 嘗試訪問 null
數組元素
對 null
數組嘗試訪問元素時也會拋出 NPE。
String[] arr = null;
String element = arr[0]; // NPE: arr 是 null,無法訪問元素
7.1.4. 傳遞 null
給不接受 null
的方法
有些方法要求傳入非 null
的參數,如果傳入 null
,可能會觸發 NPE。
public void printLength(String str) {
System.out.println(str.length()); // 如果 str 為 null,將引發 NPE
}
7.1.5. 鏈式調用中的空指針
在鏈式調用中,如果某一環節返回了 null
,而后續還對其進行方法調用,就會導致 NPE。
Person person = getPerson();
int age = person.getAddress().getCity().getZipCode(); // 如果 person 或 address 為 null,則會 NPE
7.2. 如何防止NPE問題?
7.2.1. 避免使用 null
值
盡量避免使用 null
,特別是在可能觸發 NPE 的地方。可以使用 Optional 來表示可能為空的值。
Optional<String> optionalStr = Optional.ofNullable(str);
optionalStr.ifPresent(s -> System.out.println(s.length())); // 安全訪問
7.2.2. 空值檢查
在調用對象的方法之前,先檢查對象是否為 null
。
if (str != null) {System.out.println(str.length()); // 只有 str 非 null 時才調用方法
} else {System.out.println("str is null");
}
7.2.3. 使用默認值
如果方法或字段值可能為 null
,考慮使用默認值或替代值。
String str = Optional.ofNullable(inputString).orElse("default value");
7.2.4. 適用斷言和工具庫
通過工具庫如 Apache Commons Lang 提供的 StringUtils
或 ObjectUtils
等可以避免手動編寫空值檢查代碼,減少 NPE 風險。
StringUtils.isNotEmpty(str); // 不會拋出空指針異常
7.2.5. 使用 @NonNull
和 @Nullable
注解
通過注解可以清楚標明方法參數或返回值是否可以為空,這有助于避免因不清楚空指針約束導致的 NPE。
public void processString(@NonNull String str) {// str 必須不為 null
}
7.2.6. 避免深層嵌套的鏈式調用
通過設計合理的 API 接口或引入中間變量,避免深層次的鏈式調用,降低因某一環節為 null
導致的 NPE 風險。
Address address = person != null ? person.getAddress() : null;
if (address != null) {// 安全地訪問 address
}