Effective Java 在工作中的應(yīng)用總結(jié)
一 創(chuàng)建和銷毀對(duì)象篇
1 若有多個(gè)構(gòu)造器參數(shù)時(shí),優(yōu)先考慮構(gòu)造器
當(dāng)類構(gòu)造包含多個(gè)參數(shù)時(shí),同學(xué)們會(huì)選擇 JavaBeans 模式。在這種模式下,可以調(diào)用一個(gè)無(wú)參構(gòu)造器來(lái)創(chuàng)建對(duì)象,然后調(diào)用 setter 方法來(lái)設(shè)置必要和可選的參數(shù)。目前較受歡迎的方法之一如在類上加入 Lombok 提供的@Data注解,來(lái)自動(dòng)生成getter/setter、equals 等方法。但是JavaBeans模式無(wú)法將類做成不可變(immutable,詳見(jiàn)“使可變形最小化”章節(jié))。這就需要開(kāi)發(fā)者自己掌控值的更新情況,確保線程安全等。
推薦:Builder模式
Builder 模式通過(guò) builder 對(duì)象上,調(diào)用類似 setter 的方法,設(shè)置相關(guān)的參數(shù)(類似 Proto Buffers)。最后,通過(guò)調(diào)用 build 方法來(lái)生成不可變的對(duì)象(immutable object)。使用 Builder 模式的方法之一包括在類上加入 Lombok 提供的 @Builder 注解。
應(yīng)用:API Request & Response
在微服務(wù)架構(gòu)中,服務(wù)的請(qǐng)求(request)和響應(yīng)(response)往往包含較多參數(shù)。在處理請(qǐng)求的過(guò)程中,筆者也常常會(huì)擔(dān)心誤操作修改了請(qǐng)求的內(nèi)容。所以,筆者傾向使用Builder模式。
我們可使用Builder模式來(lái)構(gòu)建該類型對(duì)象。在構(gòu)建過(guò)程中,若需要引入額外邏輯(e.g. if-else),可先返回Builder對(duì)象,最后再調(diào)用build方法。
import lombok.Builder;
/** 請(qǐng)求類 */
@Builder
public class SampleRequest {
private String paramOne;
private int paramTwo;
private boolean paramThree;
}
/** 響應(yīng)類 */
@Builder
public class SampleResponse {
private boolean success;
}
/** 服務(wù)接口 */
public interface SampleFacade {
Result<SampleResponse> rpcOne(RequestParam<SampleRequest>);
}
/** 調(diào)用 */
public void testRpcOne() {
SampleRequest request =
SampleRequest.builder().paramOne("one").paramTwo(2).paramThree(true).build();
Result<SampleResponse> response = sampleFacade.rpcOne(request);
}
2 通過(guò)私有構(gòu)造器強(qiáng)化不可實(shí)例化的能力
有些類,例如工具類(utility class),只包含靜態(tài)字段和靜態(tài)方法。這些類應(yīng)盡量確保不被實(shí)例化,防止用戶誤用。
推薦:私有化類構(gòu)造器
為了防止誤導(dǎo)用戶,認(rèn)為該類是專門為了繼承而設(shè)計(jì)的,我們可以將構(gòu)造器私有化。
public class SampleUtility {
public static String getXXX() {
return "test";
}
/** 私有化構(gòu)造器 */
private SampleUtility() {}
}
/** 直接調(diào)用方法 */
public static void main(String[] args) {
System.out.println(SampleUtility.getXXX());
}
二 類和接口篇
1 最小化類和成員的可訪問(wèn)性
盡可能地使每個(gè)類或者成員不被外界訪問(wèn)。
推薦:有的時(shí)候,為了測(cè)試,我們不得不將某些私有的(private)類、接口或者成員變成包級(jí)私有的(package-private)。這里,筆者推薦大家使用 Guava 提供的 @VisiableForTesting 注解,來(lái)提示這是為了測(cè)試而使可訪問(wèn)級(jí)別變?yōu)榘?jí)私有,放寬了限制。
import com.google.common.annotations.VisibleForTesting;
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
String getXXX() {
return "test";
}
此外,也有小伙伴推薦 PowerMock 單元測(cè)試框架。PowerMock 是 Mockito 的加強(qiáng)版,可以實(shí)現(xiàn)完成對(duì)private/static/final方法的Mock(模擬)。通過(guò)加入 @PrepareForTest 注解來(lái)實(shí)現(xiàn)。
public class Utility {
private static boolean isGreaterThan(int a, int b) {
return a > b;
}
private Utility() {}
}
/** 測(cè)試類 */
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;
@RunWith(PowerMockRunner.class)
@PrepareForTest({Utility.class})
public class UtilityTest {
@Test
public void test_privateIsGreaterThan_success() throws Exception {
/** 測(cè)試私有的 isGreaterThan 方法 */
boolean result = Whitebox.invokeMethod(Utility.class, "isGreaterThan", 3, 2);
Assertions.assertTrue(result);
}
}
2 使可變形最小化
不可變類(immutable class)是指類對(duì)應(yīng)的實(shí)例被創(chuàng)建后,就無(wú)法改變其成員變量值。即實(shí)例中包含的所有信息都必須在創(chuàng)建該實(shí)例的時(shí)候提供,并在對(duì)象的生命周期內(nèi)固定不變。
不可變類一般采用函數(shù)(
functional)模式,即對(duì)應(yīng)的方法返回一個(gè)函數(shù)的結(jié)果,函數(shù)對(duì)操作數(shù)進(jìn)行運(yùn)算但并不修改它。與之相對(duì)應(yīng)的更常見(jiàn)的是過(guò)程的(procedure)或者命令式的(imperative)做法。使用這些方法時(shí),將一個(gè)過(guò)程作用在它們的操作數(shù)上,會(huì)導(dǎo)致它的狀態(tài)發(fā)生改變。
如在“若有多個(gè)構(gòu)造器參數(shù)時(shí),優(yōu)先考慮構(gòu)造器”一節(jié)中提到,不可變對(duì)象比較簡(jiǎn)單,線程安全,只有一種狀態(tài)。使用該類的開(kāi)發(fā)者無(wú)需再做額外的工作來(lái)維護(hù)約束關(guān)系。另外,可變的對(duì)象可以有任意復(fù)雜的狀態(tài)。若 mutator 方法(e.g. update)無(wú)詳細(xì)的描述,開(kāi)發(fā)者需要自行閱讀方法內(nèi)容。筆者經(jīng)常會(huì)花費(fèi)較多時(shí)間弄清楚在某方法內(nèi),可變對(duì)象的哪些字段被更改,方法結(jié)束后會(huì)不會(huì)影響后續(xù)的對(duì)象操作。筆者推薦傳入不可變對(duì)象,基于此用更新的參數(shù)創(chuàng)建新的不可變對(duì)象返回。雖然會(huì)創(chuàng)建更多的對(duì)象,但是保證了不可變形,以及更可讀性。
推薦:Guava Collection之Immutable類
筆者在日常開(kāi)發(fā)中傾向?qū)?Immutable 類(ImmutableList,ImmutableSet,ImmuableMap)和上文提到的函數(shù)模式集合,實(shí)現(xiàn)mutator 類方法。
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
/** 推薦 */
private static final ImmutableMap<String, Integer> SAMPLE_MAP =
ImmutableMap.of("One", 1, "Two", 2);
/** 推薦:確保原input列表不會(huì)變化 */
public ImmutableList<TestObj> updateXXX(ImmutableList<TestObj> input) {
return input.stream()
.map(obj -> obj.setXXX(true))
.collect(toImmutableList());
}
/** 不推薦:改變input的信息 */
public void filterXXX(List<TestObj> input) {
input.forEach(obj -> obj.setXXX(true));
}
三 泛型篇
1 列表優(yōu)先于數(shù)組
數(shù)組是協(xié)變的(covariant),即Sub為Super的子類型,那么數(shù)組類型Sub[] 就是Super[] 的子類型;數(shù)組是具體化的,在運(yùn)行時(shí)才知道并檢查它們的元素類型約束。而泛型是不可變的和可擦除的(即編譯時(shí)強(qiáng)化它們的類型信息,并在運(yùn)行時(shí)丟棄)。
需要警惕 public static final 數(shù)組的出現(xiàn)。很有可能是個(gè)安全漏洞!
四 方法篇
1 校驗(yàn)參數(shù)的有效性
若傳遞無(wú)效的參數(shù)值給方法,這個(gè)方法在執(zhí)行復(fù)雜、耗時(shí)邏輯之前先對(duì)參數(shù)進(jìn)行了校驗(yàn)(validation),便很快就會(huì)失敗,并且可清楚地拋出適當(dāng)?shù)漠惓!H魶](méi)有校驗(yàn)它的參數(shù),就可能會(huì)在后續(xù)發(fā)生各種奇怪的異常,有時(shí)難以排查定位原因。
筆者認(rèn)為,微服務(wù)提供的API request 也應(yīng)沿用這一思想。即在API 請(qǐng)求被服務(wù)處理之前,先進(jìn)行參數(shù)校驗(yàn)。每個(gè)request應(yīng)與對(duì)應(yīng)的request validator 綁定。若參數(shù)值無(wú)效,則拋出特定的ClientException(e.g. IllegalArgumentException)。
2 謹(jǐn)慎設(shè)計(jì)方法簽名
謹(jǐn)慎地選擇方法的名稱:
- 執(zhí)行某個(gè)動(dòng)作的方法通常用動(dòng)詞或者動(dòng)詞短語(yǔ)命名:
createXXX,updateXXX,removeXXX,convertXXX,generateXXX - 對(duì)于返回
boolean值的方法,一般以is開(kāi)頭:isValid,isLive,isEnabled
避免過(guò)長(zhǎng)的參數(shù)列表:目標(biāo)是四個(gè)參數(shù),或者更少。
- 當(dāng)參數(shù)過(guò)多時(shí),筆者會(huì)使用Pair,Triple或輔助類(e.g. 靜態(tài)成員類)
public class SampleListener {
public ConsumeConcurrentlyStatus consumeMessage(String input) {
SampleResult result = generateResult(input);
...
}
private static SampleResult generateResult(String input) {
...
}
/** 輔助類 */
private static class SampleResult {
private boolean success;
private List<String> xxxList;
private int count;
}
}
3 返回零長(zhǎng)度的數(shù)組或者集合,而不是null
若一個(gè)方法返回 null 而不是零長(zhǎng)度的數(shù)組或者集合,開(kāi)發(fā)者需要加入 != null 的檢查,有時(shí)容易忘記出錯(cuò),報(bào)NullpointerException。
說(shuō)到此,筆者想額外提一下 Optional。網(wǎng)絡(luò)上有很多關(guān)于 Optional 和 null 的使用討論。Optional 允許調(diào)用者繼續(xù)一系列流暢的方法調(diào)用(e.g. stream.getFirst().orElseThrow(() -> new MyFancyException()))。以下為筆者整理的觀點(diǎn)。
/** 推薦:提示返回值可能為空。*/
public Optional<Foo> findFoo(String id);
/**
* 中立:稍顯笨重
* 可考慮 doSomething("bar", null);
* 或者重載 doSomething("bar"); 和 doSomething("bar", "baz");
**/
public Foo doSomething(String id, Optional<Bar> barOptional);
/**
* 不推薦:違背 Optional 設(shè)計(jì)的目的。
* 當(dāng) Optional 值缺省時(shí),一般有3種處理方法:1)提供代替的值;2)調(diào)用方法提供代替的值;3)拋出異常
* 這些處理方法可以在字段初始或賦值的時(shí)候處理。
**/
public class Book {
private List<Pages> pages;
private Optional<Index> index;
}
/**
* 不推薦:違背 Optional 設(shè)計(jì)的目的。
* 若為缺省值,可直接不放入列表中。
**/
List<Optional<Foo>>
五 通用程序設(shè)計(jì)篇
1 如果需要精確的答案,請(qǐng)避免使用float和double
float 和 double 類型主要用于科學(xué)工程計(jì)算。它們執(zhí)行二進(jìn)制浮點(diǎn)運(yùn)算,為了在數(shù)值范圍上提供較為精準(zhǔn)的快速近似計(jì)算。但是,它們并不能提供完全精確的結(jié)果,尤其不適合用于貨幣計(jì)算。float 或者 double 精確地表示0.1 是不可行的。
若需系統(tǒng)來(lái)記錄十進(jìn)制小數(shù)點(diǎn),可使用BigDecimal。
2 基本類型優(yōu)先于裝箱基本類型
基本類型(primitive)例如 int、double、long 和 boolean。每個(gè)基本類型都有一個(gè)對(duì)應(yīng)的引用類型,稱作裝箱基本類型(boxed primitive),對(duì)應(yīng)為Integer、Double、Long 和 Boolean。如書(shū)中提到,它們的區(qū)別如下:

/** 推薦 */
public int sum(int a, int b) {
return a + b;
}
/** 不推薦:不必要的裝箱 */
public Integer sum(Integer a, Integer b) {
return a + b;
}
若無(wú)特殊的使用場(chǎng)景,推薦總是使用基本類型。若不得不使用裝箱基本類型,注意 == 操作和 NullPointerException 異常。裝箱基本類型的使用場(chǎng)景:
- 作為集合中的元素(e.g. Set<Long>)
- 參數(shù)化類型(e.g. ThreadLocal<Long>)
- 反射的方法調(diào)用
六 異常
1 每個(gè)方法拋出的異常都要有文檔
始終要單獨(dú)地聲明受檢的異常,并且利用
Javadoc的@throws標(biāo)記,準(zhǔn)確地記錄下拋出每個(gè)異常的條件。
在日常工作中,筆者調(diào)用其他組的 API 時(shí),有時(shí)會(huì)發(fā)現(xiàn)一些意料之外的異常。良好的文檔記錄,可以幫助 API 調(diào)用者更好得處理相關(guān)的異常。文檔記錄可包括:異常的類型,異常的 error code,和描述。
2 其他
一些公司將 API 產(chǎn)生的異常分成 ClientException 和 ServerException。一般 ClientException (e.g. 無(wú)效的服務(wù) request ) 是由調(diào)用方非常規(guī)調(diào)用 API 導(dǎo)致的異常處理,可不在服務(wù)端主要的異常監(jiān)測(cè)范圍中。而 ServerException(e.g. 數(shù)據(jù)庫(kù)查詢超時(shí))是由服務(wù)端自身原因?qū)е碌膯?wèn)題,平時(shí)需要著重監(jiān)測(cè)。
引用:
Bloch, Joshua. 2018. Effective Java, 3rd Edition
NPM鏡像站全新上線:
阿里云開(kāi)源鏡像站是由阿里云提供的開(kāi)源組件、開(kāi)源操作系統(tǒng)等工具鏡像站。NPM鏡像站全新上線,提高開(kāi)發(fā)效率,讓您的構(gòu)建更加迅速。
到此這篇關(guān)于Effective Java 在工作中的應(yīng)用總結(jié)的文章就介紹到這了,更多相關(guān)Effective Java 的應(yīng)用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java阻塞隊(duì)列的實(shí)現(xiàn)及應(yīng)用
- Java 數(shù)據(jù)結(jié)構(gòu)之堆的概念與應(yīng)用
- java和Spring中觀察者模式的應(yīng)用詳解
- Java的方法和this關(guān)鍵字如何理解與應(yīng)用
- IDEA遠(yuǎn)程部署調(diào)試Java應(yīng)用程序的詳細(xì)流程
- java編程SpringSecurity入門原理及應(yīng)用簡(jiǎn)介
- java應(yīng)用開(kāi)發(fā)之JVM運(yùn)行時(shí)內(nèi)存分析
- java應(yīng)用開(kāi)發(fā)之Mybatis通過(guò)Mapper代理自定義接口的實(shí)現(xiàn)
- 深層剖析java應(yīng)用開(kāi)發(fā)中MyBayis緩存
- Java 在生活中的 10 大應(yīng)用
相關(guān)文章
如何基于SpringBoot部署外部Tomcat過(guò)程解析
這篇文章主要介紹了SpringBoot以war包形式部署到外部Tomcat過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12
Java中l(wèi)ambda表達(dá)式實(shí)現(xiàn)aop切面功能
本文主要介紹了Java中l(wèi)ambda表達(dá)式實(shí)現(xiàn)aop切面功能,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02
解決idea中Terminal終端無(wú)法執(zhí)行GIT命令+Terminal 中文亂碼問(wèn)題
這篇文章主要介紹了解決idea中Terminal終端無(wú)法執(zhí)行GIT命令+Terminal 中文亂碼問(wèn)題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07
Mybatis查不到數(shù)據(jù)查詢返回Null問(wèn)題
mybatis突然查不到數(shù)據(jù),查詢返回的都是Null,但是 select count(*) from xxx查詢數(shù)量,返回卻是正常的。好多朋友遇到這樣的問(wèn)題不知所措,下面小編通過(guò)本教程簡(jiǎn)單給大家說(shuō)明下2016-08-08
使用mybatis-plus-generator進(jìn)行代碼自動(dòng)生成的方法
這篇文章主要介紹了使用mybatis-plus-generator進(jìn)行代碼自動(dòng)生成的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06

