我們通常僅使用Java語言的小部分功能來編寫大部分程式碼,我們例項化的每個Stream以及每個作為例項字首的@Autowired註釋都足以完成我們的大部分目標。然而,有時候,我們必須轉向該語言中很少被使用的部分:該語言的隱藏部分,該部分語言通常用於特定目的。
在本文中,我們將探討4種技巧以幫助提高開發簡便性和可讀性。並非所有這些技巧都適用於每種情況,或者大多數情況。例如,可能只有少數集中方法適用於協變返回型別或者只有幾個通用類適合使用交叉通用型別的模式,而其他方法可能可提高大多數程式碼庫意圖的可讀性和清晰度。無論在何種情況下,重要的是不僅要了解這些技巧,而且要知道何時應用它們。
1.協變返回型別
即使是最引人入勝的Java操作手冊也都會包含有關繼承、介面、抽象類和方法覆蓋的介紹,但卻很少探討覆蓋方法時更復雜的可能性。例如,下面的程式碼片段對於即使是Java開發新手也不會驚訝:
public interface Animal {
public String makeNoise();
}
public class Dog implements Animal {
@Override
public String makeNoise() {
return "Woof";
}
}
public class Cat implements Animal {
@Override
public String makeNoise() {
return "Meow";
}
}
這是多型的基本概念:物件的方法可根據其介面(Animal::makeNoise)呼叫但該方法呼叫的實際行為取決於實現型別(Dog::makeNoise)。例如,下面方法的輸出將會改變,取決於Dog物件或Cat物件是否傳遞到該方法:
public class Talker {
public static void talk(Animal animal) {
System.out.println(animal.makeNoise());
}
}
Talker.talk(new Dog()); // Output: Woof
Talker.talk(new Cat()); // Output: Meow
雖然這是很多Java應用中常用的技巧,但在覆蓋方法時可能會採用另一種操作:更改返回型別。儘管這可能是無限制的覆蓋方法的方式,但對覆蓋方法的返回型別有著嚴格限制。根據Java 8 SE語言規範(第248頁):
如果一種方法宣告d 1 (包含返回型別R 1)覆蓋或隱藏另一種方法d 2 (包含返回型別R 2)的宣告,那麼d 1 必須是的返回型別替代,否則將發生編譯時錯誤。
其中return-type-substitutable (同上,第240頁)被定義為:
如果R1 無效,則R2無效
如果R1 是原始型別,則R2 與R1相同
如果R1 是引用型別,則符合以下條件之一:
a. R1 適用於d2 型別引數,它是R2.的子型別
b. R1 可通過未經檢查的轉換被轉換為R2 的子型別
c. d1 不具有與d2 相同的簽名,且R1 = |R2|
可以說,最有趣的例子是Rules 3.a.和3.b.:當覆蓋方法時,返回型別的子型別可被宣告作為覆蓋返回型別,例如:
public interface CustomCloneable {
public Object customClone();
}
public class Vehicle implements CustomCloneable {
private final String model;
public Vehicle(String model) {
this.model = model;
}
@Override
public Vehicle customClone() {
return new Vehicle(this.model);
}
public String getModel() {
return this.model;
}
}
Vehicle originalVehicle = new Vehicle("Corvette");
Vehicle clonedVehicle = originalVehicle.customClone();
System.out.println(clonedVehicle.getModel());
雖然clone()的原始返回型別是Object,但我們可在clonedVehicle呼叫getModel() (沒有顯式轉換),因為我們已經將Vehicle :: clone的返回型別重寫為Vehicle。這消除了對亂碼的必要,我們知道我們尋找的返回型別是Vehicle,即使它被宣告為Object(相當於基於先驗資訊的安全投遞,但嚴格來說不安全):
- Vehicle clonedVehicle = (Vehicle) originalVehicle.customClone();
請注意我們仍然可將Vehicle的型別宣告為Object,返回型別將恢復到Object的原始型別:
Object clonedVehicle = originalVehicle.customClone();
System.out.println(clonedVehicle.getModel()); // ERROR: getModel not a method of Object
請注意,對於通用引數,返回型別不能過載,但對於通用類,則可過載。例如,如果基類或介面方法返回 List,子類的返回型別可能覆蓋到 ArrayList,但可能不會覆蓋到List。
2.交叉通用型別
建立通用類是很好的方法來建立一組類與組合物件進行互動。例如,List 僅儲存和檢索型別T的物件,而不瞭解其包含的元素的性質,在某些情況下,我們想要限制我們的通用型別引數(T)具有特定特徵。例如,以下介面:
public interface Writer {
public void write();
}
我們可能想要建立特定Writers組合,在下面Composite Patterns:
public class WriterComposite implements Writer {
private final List writers;
public WriterComposite(List writers) {
this.writers = writer;
}
@Override
public void write() {
for (Writer writer: this.writers) {
writer.write();
}
}
}
我們現在可遍歷Writers樹,不知道我們遇到的具體Writer是standalongWriter還是Writer組合。如果我們也希望我們的組合作為reader和writer的組合怎麼辦?例如,如果我們有以下介面:
public interface Reader {
public void read();
}
我們如何將我們的WriterComposite修改為ReaderWriterComposite?有種技巧可建立新的介面ReaderWriter,來融合Reader和Writer介面:
- public interface ReaderWriter implements Reader, Writer {}
然後,我們可修改現有的WriterComposite為以下內容:
public class ReaderWriterComposite implements ReaderWriter {
private final List readerWriters;
public WriterComposite(List readerWriters) {
this.readerWriters = readerWriters;
}
@Override
public void write() {
for (Writer writer: this.readerWriters) {
writer.write();
}
}
@Override
public void read() {
for (Reader reader: this.readerWriters) {
reader.read();
}
}
}
雖然這樣做完成了我們的目標,但我們在程式碼中製造了膨脹:我們建立了一個介面,其唯一目的是將兩個現有介面合併在一起。隨著越來越多介面出現,我們會看到膨脹的組合爆炸。例如,如果我們建立新的Modifier介面,我們現在會需要createReaderModifier、WriterModifier和ReaderWriter介面。請注意,這些介面並不增加任何功能:它們只是合併現有介面。
為了消除這個膨脹,我們需要能夠指定我們的 ReaderWriterComposite 接受通用型別引數,僅當它們都是Reader和Writer時。交叉通用型別允許我們這樣做,為了指定通用型別引數,必須同時部署reader和writer介面,我們在通用型別約束之間使用&運算子:
public class ReaderWriterComposite implements Reader, Writer {
private final List readerWriters;
public WriterComposite(List readerWriters) {
this.readerWriters = readerWriters;
}
@Override
public void write() {
for (Writer writer: this.readerWriters) {
writer.write();
}
}
@Override
public void read() {
for (Reader reader: this.readerWriters) {
reader.read();
}
}
}
如果沒有膨脹繼承樹,我們現在可約束通用型別引數來部署多個介面。請注意,相同限制還可制定其中一個介面是抽象類或者具體類。例如,如果我們將writer介面更改為抽象類,類似以下:
public abstract class Writer {
public abstract void write();
}
我們仍然可限制我們的通用型別引數為Reader和Writer,但Writer(由於它是抽象類而不是介面)必須先被指定(還要注意,我們的ReaderWriterComposite現在擴充套件Writer抽象類並部署Reader介面,而不是實現兩者)
public class ReaderWriterComposite extends Writer implements Reader {
// Same class body as before
}
同樣重要的是,這種交叉通用型別可用於兩個以上介面(或者一個抽象類和多個介面),例如,如果我們想要我們的組合還包含Modifierinterface,我們可按以下編寫我們的類定義:
public class ReaderWriterComposite implements Reader, Writer, Modifier {
private final List things;
public ReaderWriterComposite(List things) {
this.things = things;
}
@Override
public void write() {
for (Writer writer: this.things) {
writer.write();
}
}
@Override
public void read() {
for (Reader reader: this.things) {
reader.read();
}
}
@Override
public void modify() {
for (Modifier modifier: this.things) {
modifier.modify();
}
}
}
雖然可執行上述,但這可能是程式碼嗅覺的跡象(Reader、Writer和Modifier物件可能是更具體的東西,例如File)
有關交叉通用型別的更多資訊,請參閱Java 8語言規範。
3.自動關閉類
建立資源類是一種常見做法,但保持該資源的完整性具有挑戰性,特別是當涉及異常處理時。例如,假設我們建立一個資源類,Resource,並希望對該資源執行操作,這可能會引發異常(例項化過程也可能會引發異常):
public class Resource {
public Resource() throws Exception {
System.out.println("Created resource");
}
public void someAction() throws Exception {
System.out.println("Performed some action");
}
public void close() {
System.out.println("Closed resource");
}
}
在任一情況下(引發異常或者沒有引發),我們要關閉我們的資源以確保沒有資源洩漏。正常的過程是在finally塊中封閉我們的 close() 方法,確保無論發生什麼情況,我們的資源在封閉執行範圍完成前關閉:
Resource resource = null;
try {
resource = new Resource();
resource.someAction();
}
catch (Exception e) {
System.out.println("Exception caught");
}
finally {
resource.close();
}
通過簡單的檢查,我們發現很多樣板程式碼從Resource物件someAction()的執行可讀性減損。為了彌補這種情況,Java 7引入try-with-resources宣告,resource可在try宣告中建立,並在離開try執行範圍前自動關閉。為了讓類可使用try-with-resources,必須部署自動關閉介面:
public class Resource implements AutoCloseable {
public Resource() throws Exception {
System.out.println("Created resource");
}
public void someAction() throws Exception {
System.out.println("Performed some action");
}
@Override
public void close() {
System.out.println("Closed resource");
}
}
我們的Resource類現在採用自動可關閉介面,我們可清理程式碼以確保資源在離開try執行範圍之前關閉。
try (Resource resource = new Resource()) {
resource.someAction();
}
catch (Exception e) {
System.out.println("Exception caught");
}
與非try-with-resource技術相比,這個過程沒有那麼混亂,並保持相同安全性(在try執行範圍完成前resource始終關閉)。如果執行上述try-with-resource,我們獲得以下輸出:
Created resource
Performed some action
Closed resource
為了展示這種try-with-resource技術的安全性,我們可改變someAction()為丟擲Exception:
public class Resource implements AutoCloseable {
public Resource() throws Exception {
System.out.println("Created resource");
}
public void someAction() throws Exception {
System.out.println("Performed some action");
throw new Exception();
}
@Override
public void close() {
System.out.println("Closed resource");
}
}
如果我們重新執行try -with-resources宣告,我們可獲得以下輸出:
Created resource
Performed some action
Closed resource
Exception caught
請注意,即使在執行someAction()方法時丟擲Exception,我們的資源仍然關閉,則Exception被捕獲。這可確保在離開try執行範圍前,我們的資源保證關閉。同樣重要的是要注意,resource可部署Closeable介面,仍然可使用try-with-resources宣告。部署自動關閉介面和可關閉介面之間的區別在於從 close() 方法簽名丟擲的Exception型別:Exceptionand IOException。在我們的例子中,我們簡單地改變了 close() 方法的簽名,不會引發異常。
4.最終類和方法
在幾乎所有情況下,我們建立的類可由另一位開發人員進行擴充套件,並根據其需求進行自定義(我們可擴充套件自己的類),即使我們不希望擴充套件我們的類。雖然這在大多數情況下是足夠的,但有時候我們不希望方法被覆蓋,或者讓我們的類被擴充套件。例如,我們建立File類來封裝檔案系統中檔案的讀取和寫入,我們可能不希望任何子類覆蓋我們的 read(int bytes) 和write(String data)方法(如果這些方法的裸機被改變,可能會導致檔案系統損壞)。在這種情況下,我們標記不可擴充套件方法作為final:
public class File {
public final String read(int bytes) {
// Execute the read on the file system
return "Some read data";
}
public final void write(String data) {
// Execute the write to the file system
}
}
現在,如果另一個類希望覆蓋讀取或寫入方法,則會引發編譯錯誤:無法從File覆蓋最終方法。我們不僅記錄我們的方法不應該被覆蓋,編譯器也確保這個意圖在編譯時不會執行。
在將這個想法擴充套件到整個類時,可能有時候我們不希望我們的類被擴充套件。這不僅會使類的每個方法不可執行,還會讓無法建立類的子型別。例如,如果我們在建立安全框架來使用金鑰生成器,我們可能不會想要任何外部開發人員擴充套件我們的金鑰生成器以及覆蓋生成演算法(自定義功能可能會影響系統):
public final class KeyGenerator {
private final String seed;
public KeyGenerator(String seed) {
this.seed = seed;
}
public CryptographicKey generate() {
// ...Do some cryptographic work to generate the key...
}
}
通過將我們的KeyGenerator類作為最終類,編譯器可確保沒有類可擴充套件我們的類以及將其傳遞到我們的框架作為有效的加密金鑰生成器。儘管簡單地標記thegenerate() 方法為最終似乎已經足夠,但這並不會阻止開發人員建立自定義金鑰生成器並將其作為有效生成器。由於我們的系統為安全導向,所以應該儘可能不要信任外部世界(聰明的開發人員可通過改變KeyGenerator類中其他方法的功能來改變生成演算法)。
雖然這似乎是對開放/封閉原則的公開否認,但這樣做有很好的理由。從我們上面安全示例中可以看出,很多時候,我們無法讓外部開發人員對我們的應用程式做想做的事情,我們必須對整合非常仔細地做決定。這個概念無處不在,例如C#語言預設一個類作為final(不能被擴充套件),並且,它必須被開發人員指定為開放。此外,我們應該非常慎重地確定哪些類可以被擴充套件,哪些方法可被覆蓋。
結論
儘管我們僅使用Java小部分功能來編寫大多數程式碼,但這足以解決我們遇到的大部分問題。有時候,我們需要深入挖掘那些被遺忘或者未知的語言部分來解決特定問題。協變返回型別和交叉通用型別等技術可用於一次性的情況,而自動關閉資源和最終方法及類的方法則可用於產生更可讀和更準確的程式碼。你可將這些技巧與日常程式設計實踐結合起來,這可幫助你更好地編寫Java程式碼。
注:加群要求 學習交流群:642830685
1、想學習JAVA這一門技術, 對JAVA感興趣零基礎,想從事JAVA工作的。
2、工作1-5年,感覺自己技術不行,想提升的
3、如果沒有工作經驗,但基礎非常紮實,想提升自己技術的。
4、還有就是想一起交流學習的。
5.小號加群一律不給過,謝謝。
轉發此文章請帶上原文連結,否則將追究法律責任