古語有云:
道為術(shù)之靈圃泡,術(shù)為道之體碟案;以道統(tǒng)術(shù),以術(shù)得道颇蜡。
其中:“道”指“規(guī)律价说、道理辆亏、理論”,“術(shù)”指“方法鳖目、技巧扮叨、技術(shù)”。意思是:“道”是“術(shù)”的靈魂领迈,“術(shù)”是“道”的肉體彻磁;可以用“道”來統(tǒng)管“術(shù)”,也可以從“術(shù)”中獲得“道”狸捅。
在拜讀大佬“孤盡”的文章《Code Review是苦澀但有意思的修行》時(shí)衷蜓,感受最深的一句話就是:“優(yōu)質(zhì)的代碼一定是少即是多的精兵原則”,這就是大佬的代碼精簡之“道”尘喝。
工匠追求“術(shù)”到極致恍箭,其實(shí)就是在尋“道”,且離悟“道”也就不遠(yuǎn)了瞧省,亦或是已經(jīng)得道扯夭,這就是“工匠精神”——一種追求“以術(shù)得道”的精神。如果一個(gè)工匠只滿足于“術(shù)”鞍匾,不能追求“術(shù)”到極致去悟“道”交洗,那只是一個(gè)靠“術(shù)”養(yǎng)家糊口的工匠而已。作者根據(jù)多年來的實(shí)踐探索橡淑,總結(jié)了大量的 Java 代碼精簡之“術(shù)”构拳,試圖闡述出心中的 Java 代碼精簡之“道”。
利用語法
1.1.利用三元表達(dá)式
普通:
String?title;if?(isMember(phone))?{????title?=?"會(huì)員";}?else?{????title?=?"游客";}
精簡:
String?title?=?isMember(phone)???"會(huì)員"?:?"游客";
注意:對于包裝類型的算術(shù)計(jì)算梁棠,需要注意避免拆包時(shí)的空指針問題置森。
1.2.利用 for-each 語句
從 Java 5 起,提供了 for-each 循環(huán)符糊,簡化了數(shù)組和集合的循環(huán)遍歷凫海。 for-each 循環(huán)允許你無需保持傳統(tǒng) for 循環(huán)中的索引就可以遍歷數(shù)組,或在使用迭代器時(shí)無需在 while 循環(huán)中調(diào)用 hasNext 方法和 next 方法就可以遍歷集合男娄。
普通:
double[] values = ...;
for(int i = 0; i < values.length; i++) {
double value = values[i];
// TODO: 處理value
}
List valueList = ...;
Iterator iterator = valueList.iterator();
while (iterator.hasNext()) {
Double value = iterator.next();
// TODO: 處理value
}
精簡:
double[] values = ...;
for(double value : values) {
// TODO: 處理value
}
List valueList = ...;
for(Double value : valueList) {
// TODO: 處理value
}
1.3.利用 try-with-resource 語句
所有實(shí)現(xiàn) Closeable 接口的“資源”行贪,均可采用 try-with-resource 進(jìn)行簡化。
普通:
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("cities.csv"));
String line;
while ((line = reader.readLine()) != null) {
// TODO: 處理line
}
} catch (IOException e) {
log.error("讀取文件異常", e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
log.error("關(guān)閉文件異常", e);
}
}
}
精簡:
try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
// TODO: 處理line
}
} catch (IOException e) {
log.error("讀取文件異常", e);
}
1.4.利用 return 關(guān)鍵字
利用 return 關(guān)鍵字模闲,可以提前函數(shù)返回建瘫,避免定義中間變量。
普通:
public?static?boolean?hasSuper(@NonNull?List?userList)?{
boolean?hasSuper?=?false;
for?(UserDO?user?:?userList)?{
if?(Boolean.TRUE.equals(user.getIsSuper()))?{
hasSuper?=?true;
break;
}
}
return?hasSuper;
}
精簡:
public?static?boolean?hasSuper(@NonNull?List?userList)?{
for?(UserDO?user?:?userList)?{
if?(Boolean.TRUE.equals(user.getIsSuper()))?{
return?true;
}
}
return?false;
}
1.5.利用 static 關(guān)鍵字
利用 static 關(guān)鍵字尸折,可以把字段變成靜態(tài)字段啰脚,也可以把函數(shù)變?yōu)殪o態(tài)函數(shù),調(diào)用時(shí)就無需初始化類對象实夹。
普通:
public?final?class?GisHelper?{
public?double?distance(double?lng1,?double?lat1,?double?lng2,?double?lat2)?{
//?方法實(shí)現(xiàn)代碼
}
}
GisHelper?gisHelper?=?new?GisHelper();
double?distance?=?gisHelper.distance(116.178692D,?39.967115D,?116.410778D,?39.899721D);
精簡:
public?final?class?GisHelper?{
public?static?double?distance(double?lng1,?double?lat1,?double?lng2,?double?lat2)?{
//?方法實(shí)現(xiàn)代碼
}
}
double?distance?=?GisHelper.distance(116.178692D,?39.967115D,?116.410778D,?39.899721D);
1.6.利用 lambda 表達(dá)式
Java 8 發(fā)布以后橄浓,lambda 表達(dá)式大量替代匿名內(nèi)部類的使用晾咪,在簡化了代碼的同時(shí),更突出了原有匿名內(nèi)部類中真正有用的那部分代碼贮配。
普通:
new?Thread(new?Runnable()?{????public?void?run()?{????????//?線程處理代碼????}}).start();
精簡:
new?Thread(()?->?{????//?線程處理代碼}).start();
1.7.利用方法引用
方法引用(::)谍倦,可以簡化 lambda 表達(dá)式,省略變量聲明和函數(shù)調(diào)用泪勒。
普通:
Arrays.sort(nameArray,?(a,?b)?->?a.compareToIgnoreCase(b));
List?userIdList?=?userList.stream()
.map(user?->?user.getId())
.collect(Collectors.toList());
精簡:
Arrays.sort(nameArray,?String::compareToIgnoreCase);
List?userIdList?=?userList.stream()
.map(UserDO::getId)
.collect(Collectors.toList());
1.8.利用靜態(tài)導(dǎo)入
靜態(tài)導(dǎo)入(import static)昼蛀,當(dāng)程序中大量使用同一靜態(tài)常量和函數(shù)時(shí),可以簡化靜態(tài)常量和函數(shù)的引用圆存。
普通:
List?areaList?=?radiusList.stream().map(r?->?Math.PI?*?Math.pow(r,?2)).collect(Collectors.toList());
精簡:
import?static?java.lang.Math.PI;
import?static?java.lang.Math.pow;
import?static?java.util.stream.Collectors.toList;
List?areaList?=?radiusList.stream().map(r?->?PI?*?pow(r,?2)).collect(toList());
注意:靜態(tài)引入容易造成代碼閱讀困難叼旋,所以在實(shí)際項(xiàng)目中應(yīng)該警慎使用。
1.9.利用 unchecked 異常
Java 的異常分為兩類:Checked 異常和 Unchecked 異常沦辙。Unchecked 異常繼承了RuntimeException 夫植,特點(diǎn)是代碼不需要處理它們也能通過編譯,所以它們稱作 Unchecked 異常油讯。利用 Unchecked 異常详民,可以避免不必要的 try-catch 和 throws 異常處理。
普通:
@Service
public?class?UserService?{
public?void?createUser(UserCreateVO?create,?OpUserVO?user)?throws?BusinessException?{
checkOperatorUser(user);
...
}
private?void?checkOperatorUser(OpUserVO?user)?throws?BusinessException?{
if?(!hasPermission(user))?{
throw?new?BusinessException("用戶無操作權(quán)限");
}
...
}
...
}
@RestController
@RequestMapping("/user")
public?class?UserController?{
@Autowired
private?UserService?userService;
@PostMapping("/createUser")
public?Result?createUser(@RequestBody?@Valid?UserCreateVO?create,?OpUserVO?user)?throws?BusinessException?{
userService.createUser(create,?user);
return?Result.success();
}
...
}
精簡:
@Service
public?class?UserService?{
public?void?createUser(UserCreateVO?create,?OpUserVO?user)?{
checkOperatorUser(user);
...
}
private?void?checkOperatorUser(OpUserVO?user)?{
if?(!hasPermission(user))?{
throw?new?BusinessRuntimeException("用戶無操作權(quán)限");
}
...
}
...
}
@RestController
@RequestMapping("/user")
public?class?UserController?{
@Autowired
private?UserService?userService;
@PostMapping("/createUser")
public?Result?createUser(@RequestBody?@Valid?UserCreateVO?create,?OpUserVO?user)?{
userService.createUser(create,?user);
return?Result.success();
}
...
}
利用注解
2.1.利用 Lombok 注解
Lombok 提供了一組有用的注解陌兑,可以用來消除Java類中的大量樣板代碼沈跨。
普通:
public?class?UserVO?{
private?Long?id;
private?String?name;
public?Long?getId()?{
return?this.id;
}
public?void?setId(Long?id)?{
this.id?=?id;
}
public?String?getName()?{
return?this.name;
}
public?void?setName(String?name)?{
this.name?=?name;
}
...
}
精簡:
@Getter
@Setter
@ToString
public?class?UserVO?{
private?Long?id;
private?String?name;
...
}
2.2.利用 Validation 注解
普通:
@Getter@Setter@ToStringpublic?class?UserCreateVO?{
@NotBlank(message?=?"用戶名稱不能為空")
private?String?name;
@NotNull(message?=?"公司標(biāo)識(shí)不能為空")
private?Long?companyId;
...
}
@Service@Validatedpublic?class?UserService?{
public?Long?createUser(@Valid?UserCreateVO?create)?{
//?TODO:?創(chuàng)建用戶
return?null;
}
}
精簡:
@Getter
@Setter
@ToString
public?class?UserCreateVO?{
@NotBlank(message?=?"用戶名稱不能為空")
private?String?name;
@NotNull(message?=?"公司標(biāo)識(shí)不能為空")
private?Long?companyId;
...
}
@Service
@Validated
public?class?UserService?{
public?Long?createUser(@Valid?UserCreateVO?create)?{
//?TODO:?創(chuàng)建用戶
return?null;
}
}
2.3.利用 @NonNull 注解
Spring 的 @NonNull 注解,用于標(biāo)注參數(shù)或返回值非空兔综,適用于項(xiàng)目內(nèi)部團(tuán)隊(duì)協(xié)作饿凛。只要實(shí)現(xiàn)方和調(diào)用方遵循規(guī)范,可以避免不必要的空值判斷软驰,這充分體現(xiàn)了阿里的“新六脈神劍”提倡的“因?yàn)樾湃谓е希院唵巍薄?/p>
普通:
public?List?queryCompanyUser(Long?companyId)?{
//?檢查公司標(biāo)識(shí)
if?(companyId?==?null)?{
return?null;
}
//?查詢返回用戶
List?userList?=?userDAO.queryByCompanyId(companyId);
return?userList.stream().map(this::transUser).collect(Collectors.toList());
}
Long?companyId?=?1L;
List?userList?=?queryCompanyUser(companyId);
if?(CollectionUtils.isNotEmpty(userList))?{
for?(UserVO?user?:?userList)?{
//?TODO:?處理公司用戶
}
}
精簡:
public?@NonNull?List?queryCompanyUser(@NonNull?Long?companyId)?{
List?userList?=?userDAO.queryByCompanyId(companyId);
return?userList.stream().map(this::transUser).collect(Collectors.toList());
}
Long?companyId?=?1L;
List?userList?=?queryCompanyUser(companyId);
for?(UserVO?user?:?userList)?{
//?TODO:?處理公司用戶
}
2.4.利用注解特性
注解有以下特性可用于精簡注解聲明:
1、當(dāng)注解屬性值跟默認(rèn)值一致時(shí)锭亏,可以刪除該屬性賦值纠吴;
2、當(dāng)注解只有value屬性時(shí)贰镣,可以去掉value進(jìn)行簡寫呜象;
3膳凝、當(dāng)注解屬性組合等于另一個(gè)特定注解時(shí)碑隆,直接采用該特定注解。
普通:
@Lazy(true);
@Service(value?=?"userService")
@RequestMapping(path?=?"/getUser",?method?=?RequestMethod.GET)
精簡:
@Lazy
@Service("userService")
@GetMapping("/getUser")
利用泛型
3.1.泛型接口
在 Java 沒有引入泛型前蹬音,都是采用 Object 表示通用對象上煤,最大的問題就是類型無法強(qiáng)校驗(yàn)并且需要強(qiáng)制類型轉(zhuǎn)換。
普通:
public?interface?Comparable?{
public?int?compareTo(Object?other);
}
@Getter
@Setter
@ToString
public?class?UserVO?implements?Comparable?{
private?Long?id;
@Override
public?int?compareTo(Object?other)?{
UserVO?user?=?(UserVO)other;
return?Long.compare(this.id,?user.id);
}
}
精簡:
public?interface?Comparable?{
public?int?compareTo(T?other);
}
@Getter
@Setter
@ToString
public?class?UserVO?implements?Comparable?{
private?Long?id;
@Override
public?int?compareTo(UserVO?other)?{
return?Long.compare(this.id,?other.id);
}
}
3.2.泛型類
普通:
@Getter
@Setter
@ToString
public?class?IntPoint?{
private?Integer?x;
private?Integer?y;
}
@Getter
@Setter
@ToString
public?class?DoublePoint?{
private?Double?x;
private?Double?y;
}
精簡:
@Getter
@Setter
@ToString
public?class?Point?{
private?T?x;
private?T?y;
}
3.3.泛型方法
普通:
public?static?Map?newHashMap(String[]?keys,?Integer[]?values)?{
//?檢查參數(shù)非空
if?(ArrayUtils.isEmpty(keys)?||?ArrayUtils.isEmpty(values))?{
return?Collections.emptyMap();
}
//?轉(zhuǎn)化哈希映射
Map?map?=?new?HashMap<>();
int?length?=?Math.min(keys.length,?values.length);
for?(int?i?=?0;?i?<?length;?i++)?{
map.put(keys[i],?values[i]);
}
return?map;
}
精簡:
public?static??Map?newHashMap(K[]?keys,?V[]?values)?{
//?檢查參數(shù)非空
if?(ArrayUtils.isEmpty(keys)?||?ArrayUtils.isEmpty(values))?{
return?Collections.emptyMap();
}
//?轉(zhuǎn)化哈希映射
Map?map?=?new?HashMap<>();
int?length?=?Math.min(keys.length,?values.length);
for?(int?i?=?0;?i?<?length;?i++)?{
map.put(keys[i],?values[i]);
}
return?map;
}
利用自身方法
4.1.利用構(gòu)造方法
構(gòu)造方法著淆,可以簡化對象的初始化和設(shè)置屬性操作劫狠。對于屬性字段較少的類拴疤,可以自定義構(gòu)造方法。
普通:
@Getter
@Setter
@ToString
public?class?PageDataVO?{
private?Long?totalCount;
private?List?dataList;
}
PageDataVO?pageData?=?new?PageDataVO<>();
pageData.setTotalCount(totalCount);
pageData.setDataList(userList);
return?pageData;
...
精簡:
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public?class?PageDataVO?{
private?Long?totalCount;
private?List?dataList;
}
return?new?PageDataVO<>(totalCount,?userList);
注意:如果屬性字段被替換時(shí)独泞,存在構(gòu)造函數(shù)初始化賦值問題呐矾。比如把屬性字段title替換為 nickname ,由于構(gòu)造函數(shù)的參數(shù)個(gè)數(shù)和類型不變懦砂,原有構(gòu)造函數(shù)初始化語句不會(huì)報(bào)錯(cuò)蜒犯,導(dǎo)致把原title值賦值給 nickname 。如果采用 Setter 方法賦值荞膘,編譯器會(huì)提示錯(cuò)誤并要求修復(fù)罚随。
4.2.利用 Set 的 add 方法
利用 Set 的 add 方法的返回值,可以直接知道該值是否已經(jīng)存在羽资,可以避免調(diào)用 contains 方法判斷存在淘菩。
普通:
以下案例是進(jìn)行用戶去重轉(zhuǎn)化操作,需要先調(diào)用 contains 方法判斷存在屠升,后調(diào)用add方法進(jìn)行添加潮改。
Set?userIdSet?=?new?HashSet<>();
List?userVOList?=?new?ArrayList<>();
for?(UserDO?userDO?:?userDOList)?{
if?(!userIdSet.contains(userDO.getId()))?{
userIdSet.add(userDO.getId());
userVOList.add(transUser(userDO));
}
}
精簡:
Set?userIdSet?=?new?HashSet<>();
List?userVOList?=?new?ArrayList<>();
for?(UserDO?userDO?:?userDOList)?{
if?(userIdSet.add(userDO.getId()))?{
userVOList.add(transUser(userDO));
}
}
4.3.利用 Map 的 computeIfAbsent 方法
利用 Map 的 computeIfAbsent 方法,可以保證獲取到的對象非空腹暖,從而避免了不必要的空判斷和重新設(shè)置值进陡。
普通:
Map>?roleUserMap?=?new?HashMap<>();
for?(UserDO?userDO?:?userDOList)?{
Long?roleId?=?userDO.getRoleId();
List?userList?=?roleUserMap.get(roleId);
if?(Objects.isNull(userList))?{
userList?=?new?ArrayList<>();
roleUserMap.put(roleId,?userList);
}
userList.add(userDO);
}
精簡:
Map>?roleUserMap?=?new?HashMap<>();
for?(UserDO?userDO?:?userDOList)?{
roleUserMap.computeIfAbsent(userDO.getRoleId(),?key?->?new?ArrayList<>())
.add(userDO);
}
4.4.利用鏈?zhǔn)骄幊?/b>
鏈?zhǔn)骄幊蹋步屑?jí)聯(lián)式編程微服,調(diào)用對象的函數(shù)時(shí)返回一個(gè)this對象指向?qū)ο蟊旧碇壕危_(dá)到鏈?zhǔn)叫Ч梢约?jí)聯(lián)調(diào)用以蕴。鏈?zhǔn)骄幊痰膬?yōu)點(diǎn)是:編程性強(qiáng)糙麦、可讀性強(qiáng)、代碼簡潔丛肮。
普通:
StringBuilder?builder?=?new?StringBuilder(96);
builder.append("select?id,?name?from?");
builder.append(T_USER);
builder.append("?where?id?=?");
builder.append(userId);
builder.append(";");
精簡:
StringBuilder?builder?=?new?StringBuilder(96);
builder.append("select?id,?name?from?")
.append(T_USER)
.append("?where?id?=?")
.append(userId)
.append(";");
利用工具方法
5.1.避免空值判斷
普通:
if?(userList?!=?null?&&?!userList.isEmpty())?{????//?TODO:?處理代碼}
精簡:
if?(CollectionUtils.isNotEmpty(userList))?{????//?TODO:?處理代碼}
5.2.避免條件判斷
普通:
double?result;if?(value?<=?MIN_LIMIT)?{????result?=?MIN_LIMIT;}?else?{????result?=?value;}
精簡:
double?result?=?Math.max(MIN_LIMIT,?value);
5.3.簡化賦值語句
普通:
public?static?final?List?ANIMAL_LIST;
static?{
List?animalList?=?new?ArrayList<>();
animalList.add("dog");
animalList.add("cat");
animalList.add("tiger");
ANIMAL_LIST?=?Collections.unmodifiableList(animalList);
}
精簡:
//?JDK流派
public?static?final?List?ANIMAL_LIST?=?Arrays.asList("dog",?"cat",?"tiger");
//?Guava流派
public?static?final?List?ANIMAL_LIST?=?ImmutableList.of("dog",?"cat",?"tiger");
注意:Arrays.asList 返回的 List 并不是 ArrayList 赡磅,不支持 add 等變更操作。
5.4.簡化數(shù)據(jù)拷貝
普通:
UserVO?userVO?=?new?UserVO();
userVO.setId(userDO.getId());
userVO.setName(userDO.getName());
...
userVO.setDescription(userDO.getDescription());
userVOList.add(userVO);
精簡:
UserVO?userVO?=?new?UserVO();
BeanUtils.copyProperties(userDO,?userVO);
userVOList.add(userVO);
反例:
List<UserVO>?userVOList?=?JSON.parseArray(JSON.toJSONString(userDOList),?UserVO.class);
精簡代碼宝与,但不能以過大的性能損失為代價(jià)焚廊。例子是淺層拷貝,用不著 JSON 這樣重量級(jí)的武器习劫。
5.5.簡化異常斷言
普通:
if?(Objects.isNull(userId))?{????throw?new?IllegalArgumentException("用戶標(biāo)識(shí)不能為空");}
精簡:
Assert.notNull(userId,?"用戶標(biāo)識(shí)不能為空");
注意:可能有些插件不認(rèn)同這種判斷咆瘟,導(dǎo)致使用該對象時(shí)會(huì)有空指針警告。
5.6.簡化測試用例
把測試用例數(shù)據(jù)以 JSON 格式存入文件中诽里,通過 JSON 的 parseObject 和 parseArray 方法解析成對象袒餐。雖然執(zhí)行效率上有所下降,但可以減少大量的賦值語句,從而精簡了測試代碼灸眼。
普通:
@Test
public?void?testCreateUser()?{
UserCreateVO?userCreate?=?new?UserCreateVO();
userCreate.setName("Changyi");
userCreate.setTitle("Developer");
userCreate.setCompany("AMAP");
...
Long?userId??=?userService.createUser(OPERATOR,?userCreate);
Assert.assertNotNull(userId,?"創(chuàng)建用戶失敗");
精簡:
@Test
public?void?testCreateUser()?{
String?jsonText?=?ResourceHelper.getResourceAsString(getClass(),?"createUser.json");
UserCreateVO?userCreate?=?JSON.parseObject(jsonText,?UserCreateVO.class);
Long?userId??=?userService.createUser(OPERATOR,?userCreate);
Assert.assertNotNull(userId,?"創(chuàng)建用戶失敗");
}
建議:JSON 文件名最好以被測試的方法命名卧檐,如果有多個(gè)版本可以用數(shù)字后綴表示。
5.7.簡化算法實(shí)現(xiàn)
一些常規(guī)算法焰宣,已有現(xiàn)成的工具方法霉囚,我們就沒有必要自己實(shí)現(xiàn)了。
普通:
int?totalSize?=?valueList.size();
List>?partitionList?=?new?ArrayList<>();
for?(int?i?=?0;?i?<?totalSize;?i?+=?PARTITION_SIZE)?{
partitionList.add(valueList.subList(i,?Math.min(i?+?PARTITION_SIZE,?totalSize)));
}
精簡:
List<List<Integer>>?partitionList?=?ListUtils.partition(valueList,?PARTITION_SIZE);
5.8.封裝工具方法
一些特殊算法匕积,沒有現(xiàn)成的工具方法佛嬉,我們就只好自己親自實(shí)現(xiàn)了。
普通:
比如闸天,SQL 設(shè)置參數(shù)值的方法就比較難用暖呕,setLong 方法不能設(shè)置參數(shù)值為 null 。
//?設(shè)置參數(shù)值
if?(Objects.nonNull(user.getId()))?{
statement.setLong(1,?user.getId());
}?else?{
statement.setNull(1,?Types.BIGINT);
}
...
精簡:
我們可以封裝為一個(gè)工具類 SqlHelper 苞氮,簡化設(shè)置參數(shù)值的代碼湾揽。
/**?SQL輔助類?*/
public?final?class?SqlHelper?{
/**?設(shè)置長整數(shù)值?*/
public?static?void?setLong(PreparedStatement?statement,?int?index,?Long?value)?throws?SQLException?{
if?(Objects.nonNull(value))?{
statement.setLong(index,?value.longValue());
}?else?{
statement.setNull(index,?Types.BIGINT);
}
}
...
}
//?設(shè)置參數(shù)值
SqlHelper.setLong(statement,?1,?user.getId());
最后:
建議默認(rèn)編寫出可讀和簡單的代碼。如果你真的發(fā)現(xiàn)存在性能問題并已經(jīng)找出它的位置笼吟,那么仍然有很多選擇來對此進(jìn)行處理而不必為了追求快速而寫出復(fù)雜的代碼库物。不到萬不得已不要為了性能而犧牲簡潔性,同時(shí)要學(xué)會(huì)始終用分析工具來處理性能問題贷帮。