我坦白\: 我喜歡 Java。
我真的喜歡!
也許這并不會讓你感到吃驚,因?yàn)槲耶吘勾_實(shí)參與編著過一本滿是 Java 代碼的書。但是事實(shí)上,當(dāng)我開始編寫 Android 應(yīng)用的時(shí)候我并不是一個(gè)喜歡 Java 的人,而當(dāng)我開始編寫書蟲編程指南的時(shí)候,我也很難稱得上是粉絲,甚至當(dāng)我們完成編寫的時(shí)候,我也始終不能算是一名超級粉絲。這個(gè)事實(shí)其實(shí)讓我自己都很吃驚!
我原本并非想抱怨什么,也并非要深刻反思一番。但是下面列出的這些內(nèi)容卻是一直困擾我的問題:
Java 很冗長。沒有任何簡短的類似 Blocks 或者 Lambda 表達(dá)式的語法來執(zhí)行回調(diào)(當(dāng)然,Java8已經(jīng)開始支持這一特性),所以你必須編寫非常多的模板代碼來實(shí)現(xiàn),有時(shí)甚至只是一個(gè)簡單的接口。如果你需要一個(gè)對象來保存四個(gè)屬性,你必須創(chuàng)建一個(gè)擁有四個(gè)命名字段的類。
這是我眼中的Java,它的代碼就像這樣:
public class NumberStack {
List<Integer> mNumbers = new ArrayList<Integer>();
public void pushNumber(int number) {
mNumbers.add(number);
}
public Integer popNumber() {
if (mNumber.size() == 0) {
return null;
} else {
return mNumber.remove(mNumber.size() - 1);
}
}
}
我學(xué)習(xí)過并且會在工作中混合使用一些內(nèi)部類和接口。雖然編寫Java程序這并不是世界上最糟糕的事情,但是我還是希望Java能夠擁有其他語言的特點(diǎn)和靈活性。類似 “天啊,我多么希望這能更像 Java” 的感嘆從沒有出現(xiàn)過。
但是,我的想法改變了。
說來也奇怪,改變我想法的恰恰是 Java 獨(dú)有的特性。請思考下面的代碼:
public class Payroll {
...
public long getWithholding(long payInDollars) {
...
return withholding;
}
public long getAfterTaxPay(Employee employee) {
long basePay = EmployeeDatabase.getInstance()
.getBasePay(employee);
long withholding = getWithholding(basePay);
return basePay - withholding;
}
}
這個(gè)類在 getAfterTaxPay()
方法中需要依賴一個(gè) EmployeeDatabase
對象。有很多種方式可以創(chuàng)建該對象,但在這個(gè)例子中, 我使用了單例模式,調(diào)用一個(gè)靜態(tài)的 getInstance 方法。
Java 中的依賴關(guān)系是非常嚴(yán)格的。所以任何時(shí)間我都像這樣編寫代碼:
long basePay = EmployeeDatabase.getInstance()
.getBasePay(employee);
在 EmployeeDatabase
類中我創(chuàng)建了一個(gè)嚴(yán)格依賴。不僅如此,我是利用EmployeeDatabase
類的特定方法 getInstance()
創(chuàng)建的嚴(yán)格依賴。而在其他語言里,我也許可以使用 swizzle 或者 monkey patch 的方式來處理這樣的事情.當(dāng)然并不是說這樣的方法有什么好處,但它至少存在實(shí)現(xiàn)的可能。但是在 Java 里是不可能的。
而創(chuàng)建依賴的其他方式比這更加嚴(yán)格。就讓我們來看看下面這行:
long basePay = new EmployeeDatabase()
.getBasePay(employee);
當(dāng)使用關(guān)鍵字 new 時(shí),我會采用與調(diào)用靜態(tài)方法相同的方式,但有一點(diǎn)不同:調(diào)用 new EmployeeDatabase()
方法一定會返回給我們一個(gè) EmployeeDatabase
類的實(shí)例。無論你如何努力,你都沒有辦法重寫這個(gè)構(gòu)造函數(shù)來讓它返回一個(gè) mock 的子類對象。
我們解決此類問題通常采用依賴注入技術(shù)。它并非 Java 獨(dú)有的特性,但對于上述提到的問題,Java 尤其需要這個(gè)特性。
依賴注入簡單的說,就是接受合作對象作為構(gòu)造方法的參數(shù)而不是直接獲取它們自身。所以 Payroll
類的實(shí)現(xiàn)會相應(yīng)地變成這樣:
public class Payroll {
...
EmployeeDatabase mEmployeeDatabase;
public Payroll(EmployeeDatabase employeeDatabase) {
mEmployeeDatabase = employeeDatabase;
}
public long getWithholding(long payInDollars) {
...
return withholding;
}
public long getAfterTaxPay(Employee employee) {
long basePay = mEmployeeDatabase.getBasePay(employee);
long withholding = getWithholding(basePay);
return basePay - withholding;
}
}
EmployeeDatabase
是一個(gè)單例?一個(gè)模擬出來的子類?還是一個(gè)上下文相關(guān)的實(shí)現(xiàn)? Payroll
類不再需要知道這些。
上述這些僅僅介紹了我真正要講的內(nèi)容——依賴注入器。
(旁白:我知道在真正開始討論前將這兩個(gè)問題講的比較深入是很奇怪的,但是我希望你們能夠容忍我這么做。正確的理解 Java 比起其他語言要花費(fèi)更多地時(shí)間。困難的事物往往都是這樣。)
現(xiàn)在我們通過構(gòu)造函數(shù)傳遞依賴,會導(dǎo)致我們的對象更加難以使用,同時(shí)也很難作出更改。在我使用依賴注入之前,我會像這樣使用 Payroll
類:
new Payroll().getAfterTaxPay(employee);
但是,現(xiàn)在我必須這樣寫:
new Payroll(EmployeeDatabase.getInstance())
.getAfterTaxPay(employee);
還有,任何時(shí)候如何我改變了 Payroll
的依賴, 我都不得不修改使用了 new Payroll
的每一個(gè)地方。
而依賴注入器允許我不再編寫用來明確提供依賴的代碼。相反,我可以直接聲明我的依賴對象,讓工具來自動(dòng)處理相應(yīng)操作。有很多依賴注入的工具,下面我將用 RoboGuice 來舉個(gè)例子。
為了這樣做,我使用“注解“這一 Java 工具來描述代碼。我們通過為構(gòu)造函數(shù)添加簡單的注解聲明:
@Inject
public Payroll(EmployeeDatabase employeeDatabase) {
mEmployeeDatabase = employeeDatabase;
}
注解 @Inject
的含義是“創(chuàng)建一個(gè) Payroll
類的實(shí)例,執(zhí)行它的構(gòu)造方法,傳遞所有的參數(shù)值。”而之后當(dāng)我真的需要一個(gè) Payroll
實(shí)例的時(shí)候,我會利用依賴注入器來幫我創(chuàng)建,就像這樣:
Payroll payroll = RoboGuice.getInjector(getContext())
.getInstance(Payroll.class);
long afterTaxPay = payroll.getAfterTaxPay(employee);
一旦我采用這種方式創(chuàng)建實(shí)例,就能使用注入器來設(shè)置足夠令人滿意的依賴。是否需要 EmployeeDatabase
是一個(gè)單例?是否需要一個(gè)可自定義的子類?所有這些都可以在同一個(gè)地方指定。
這是一種很容易使用的描述工具,但是很難比較在 Java 中是否使用依賴注入的根本差距。如果沒有依賴注入器,重構(gòu)和測試驅(qū)動(dòng)開發(fā)會是一項(xiàng)艱苦的勞動(dòng)。而使用它,這些工作則會毫不費(fèi)力。對于一名 Java 開發(fā)者來說,唯一比依賴注入器更重要的就是一個(gè)優(yōu)秀的 IDE 了。
不過,這只是廣泛可能性中的第一點(diǎn)。 對于 Google 之外的 Android 開發(fā)者來說,最令人興奮的就是基于注解的 API 了。
舉個(gè)例子,我們可以使用 ButtreKnife。通常情況下,我們會花費(fèi)大量的時(shí)間為 Android 的視圖對象編寫監(jiān)聽器,就像這樣:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_content);
View okButton = findViewById(R.id.ok_button);
okButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
onOkButtonClicked();
}
});
}
public void onOkButtonClicked() {
// 處理按鈕點(diǎn)擊
}
ButterKnife 允許我們只提供很少的代碼來描述“在 ID 為 R.id.ok_button
的視圖控件被點(diǎn)擊時(shí)調(diào)用 onOkButtonClicked
方法”這件事情,就像這樣:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_content);
ButterKnife.inject(this);
}
@OnClick(R.id.ok_button);
public void onOkButtonClicked() {
// 處理按鈕點(diǎn)擊
}
我能繼續(xù)寫很多這樣的例子。有很多庫可以通過注解來實(shí)現(xiàn)序列化與反序列化 Json,在 savedInstanceState
方法內(nèi)部存儲字段,或者是生成 REST 網(wǎng)絡(luò)服務(wù)的接口代碼等操作。
盡管有些使用注解的工具會產(chǎn)生相似的效果,不過 Java 允許使用不同的方式實(shí)現(xiàn)。下面我用 RoboGuice 和 Dagger 來舉個(gè)例子。它們都是依賴注入器,也同樣都使用 @Inject
注解。但是 RoboGuice 會在運(yùn)行時(shí)讀取你的代碼注解,而 Dragger 則是在編譯時(shí)生成對應(yīng)的代碼。
這樣會有一些重要的好處。它能在更早的時(shí)間發(fā)現(xiàn)注解中的語義錯(cuò)誤。Dagger 能夠在編譯時(shí)提醒你可能存在的循環(huán)依賴,但是 RoboGuice 不能。
而且這對提高性能也很有幫助。使用預(yù)先生成的代碼可以減少啟動(dòng)時(shí)間,并在運(yùn)行時(shí)避免讀取注解。因?yàn)樽x取注解需要使用 Java 反射相關(guān)的 API,這在 Android 設(shè)備上是很耗時(shí)的。
我會通過展示一個(gè)如何定義和處理運(yùn)行時(shí)注解的簡單例子,來結(jié)束今天的內(nèi)容。 假設(shè)你是一個(gè)很沒有耐心地人,并且厭倦了在你的 Android 程序中打出一個(gè)完整的靜態(tài)限定常量,比如:
public class CrimeActivity {
public static final String ACTION_VIEW_CRIME =
“com.bignerdranch.android.criminalintent.CrimeActivity.ACTION_VIEW_CRIME”;
}
你可以使用一個(gè)運(yùn)行時(shí)注解來幫你做這些事情。首先,你要?jiǎng)?chuàng)建一個(gè)注解類:
@Retention(RetentionPolicy.RUNTIME)
@Target( { ElementType.FIELD })
public @interface ServiceConstant { }
這段代碼聲明了一個(gè)名為 ServiceConstant
的注解。 而代碼本身被 @Retention
、@Target
注解。@Retention
表示注解將會停留的時(shí)間。在這里我們將它設(shè)置為運(yùn)行時(shí)觸發(fā)。如果我們想僅僅在編譯時(shí)處理注解,可以將其設(shè)置為 RetentionPolicy.SOURCE
。
另一個(gè)注解 @Target
,表示你放置注解的位置。當(dāng)然有很多的數(shù)據(jù)類型可以選擇。因?yàn)槲覀兊淖⒔鈨H需要對字段有效,所以只需要提供 ElementType.FIELD
的聲明。
一旦定義了注解,我們接著就要寫些代碼來尋找并自動(dòng)填充帶注解的字段:
public static void populateConstants(Class<?> klass) {
String packageName = klass.getPackage().getName();
for (Field field : klass.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) &&
field.isAnnotationPresent(ServiceConstant.class)) {
String value = packageName + "." + field.getName();
try {
field.set(null, value);
Log.i(TAG, "Setup service constant: " + value + "");
} catch (IllegalAccessException iae) {
Log.e(TAG, "Unable to setup constant for field " +
field.getName() +
" in class " + klass.getName());
}
}
}
}
最后,我們?yōu)榇a增加注解,然后調(diào)用我們充滿魔力的方法:
public class CrimeActivity {
@ServiceConstant
public static final String ACTION_VIEW_CRIME;
static {
ServiceUtils.populateConstants(CrimeActivity.class);
}
這些就是我了解的全部內(nèi)容。有太多與 Java 注解相關(guān)的部分。我不能保證所有這些能夠立刻讓你對 Java 的感受變得和我一樣,但是我希望你能確實(shí)看到很多有趣的東西。雖然通常 Java 在表達(dá)性上還欠缺一些,但是在 Java 的工具包中有一些基本的構(gòu)建模塊,能夠讓高級開發(fā)人員可以構(gòu)建更強(qiáng)大的工具,從而擴(kuò)大整個(gè)社區(qū)的生產(chǎn)力。
如果你對此很感興趣,并且打算深入了解這些,你會發(fā)現(xiàn)通過注解驅(qū)動(dòng)代碼生成的過程非常有趣。有時(shí)候并不一定要真的閱讀或者寫出漂亮的代碼,但是人們可以利用這些工具創(chuàng)造出漂亮的代碼。假如你對于實(shí)際場景如何應(yīng)用依賴注入的原理很感興趣的話,ButterKnife 的源碼還是相當(dāng)簡單的。