摘要:首當其沖的便是接口中的每個聲明必須是即便不指定也是,并且不能設置為非,詳細規(guī)則可參考可見性部分介紹。函數(shù)式接口有著不同的場景,并被認為是對編程語言的一種強大的擴展。抽象類與中的接口有些類似,與中支持默認方法的接口更為相像。
原文鏈接:http://www.javacodegeeks.com/2015/09/how-to-design-classes-and-interfaces.html
本文是Java進階課程的第三篇。
本課程的目標是幫你更有效的使用Java。其中討論了一些高級主題,包括對象的創(chuàng)建、并發(fā)、序列化、反射以及其他高級特性。本課程將為你的精通Java的旅程提供幫助。
內容綱要引言
接口
標記性接口
函數(shù)式接口,默認方法及靜態(tài)方法
抽象類
不可變類
匿名類
可見性
繼承
多重繼承
繼承與組合
封裝
Final類和方法
源碼下載
下章概要
引言不管使用哪種編程語言(Java也不例外),遵循好的設計原則是你編寫干凈、易讀、易測試代碼的關鍵,并且在程序的整個生命周期中,可提高后期的可維護性。在本章中,我們將從Java語言提供的基礎構造模塊開始,并引入一組有助于你設計出優(yōu)秀結構的設計原則。
具體包括:接口和接口的默認方法(Java 8新特性),抽象類、final類和不可變類,繼承和組合以及在對象的創(chuàng)建與銷毀中介紹過的可見性(訪問控制)規(guī)則。
接口在面向對象編程中,接口構成了基于契約的開發(fā)過程的基礎組件。簡而言之,接口定義了一組方法(契約),每個支持該接口的具體類都必須提供這些方法的實現(xiàn)。這是開發(fā)過程中一種簡單卻強有力的理念。
很多編程語言有一種或多種接口實現(xiàn)形式,而Java語言則提供了語言級的支持。下面簡單看一下Java中的接口定義形式:
package com.javacodegeeks.advanced.design; public interface SimpleInterface { void performAction(); }
在上面的代碼片段中,命名為SimpleInterface的接口只定義了一個方法performAction。接口與類的主要區(qū)別就在于接口定義了約定(聲明方法),但不為他們提供具體實現(xiàn)。
在Java中,接口的用法非常豐富:可以嵌套包含其他接口、類、枚舉和注解(枚舉和注解將在枚舉和注解的使用中介紹)以及常量,如下:
package com.javacodegeeks.advanced.design; public interface InterfaceWithDefinitions { String CONSTANT = "CONSTANT"; enum InnerEnum { E1, E2; } class InnerClass { } interface InnerInterface { void performInnerAction(); } void performAction(); }
針對上面的復雜場景,Java編譯器強制為嵌套的類對象構造和方法聲明提供了一組隱式的要求。首當其沖的便是接口中的每個聲明必須是public(即便不指定也是public,并且不能設置為非public,詳細規(guī)則可參考可見性部分介紹)。所以下面代碼中的用法與上面看到的聲明是等價的:
public void performAction(); void performAction();
另外,接口中定義的每個方法都被默認聲明為abstract的,所以下面的聲明都是等價的:
public abstract void performAction(); public void performAction(); void performAction();
對于常量字段,除了隱式的public外,也被加上了static和final修飾,所以下面的聲明也是等價的:
String CONSTANT = "CONSTANT"; public static final String CONSTANT = "CONSTANT";
對于嵌套的類、接口或枚舉的定義,也隱式的聲明為static的,所以下面的聲明也是等價的:
class InnerClass { } static class InnerClass { }
根據(jù)個人偏好可以使用任意的聲明風格,不過了解上面的約定倒是可以減少一些不必要的代碼編寫。
標記性接口標記性接口是接口的一種特殊形式:即沒有任何方法或其他嵌套定義。在使用Object的通用方法章節(jié)中我們已經(jīng)見過這種接口:Cloneable,下面是它的定義:
public interface Cloneable { }
標記性接口并不像普通接口聲明一些契約,但卻為類“附加”或"綁定"特定的特性提供了支持。例如對于Cloneable,實現(xiàn)了此接口的類就會被認為具有克隆的能力,盡管如何克隆并未在Cloneable中定義。另外一個廣泛使用的標記性接口是Serializable:
public interface Serializable { }
這個接口聲明類可以被序列化或反序列化,同樣它并未指定序列化過程中使用的方法。
盡管標記性接口并不滿足接口作為契約的主要用途,不過在面向對象設計過程種仍然有一定的用武之地。
函數(shù)式接口,默認方法及靜態(tài)方法伴隨著Java 8的發(fā)布,接口被賦予了新的能力:靜態(tài)方法、默認方法以及從lambda表達式的自動轉換(函數(shù)式接口)。
在上面的接口部分,我們強調過在Java中接口只能作為聲明但不能提供任何實現(xiàn)。但默認方法打破了這一原則:在接口中可以為default標記的方法提供實現(xiàn),如下:
package com.javacodegeeks.advanced.design; public interface InterfaceWithDefaultMethods { void performAction(); default void performDefaulAction() { // Implementation here } }
從對象實例層次看,默認方法可被任何的接口實現(xiàn)者重載;除此之外,接口還提供了另外的靜態(tài)方法,如下:
package com.javacodegeeks.advanced.design; public interface InterfaceWithDefaultMethods { static void createAction() { // Implementation here } }
也許你會認為在接口中提供實現(xiàn)違背了基于契約的開發(fā)過程,不也你也可以列出很多Java把這些特性引入其中的理由。不管是帶來了幫助還是困擾,它們已然存在,你也可以使用它們。
函數(shù)式接口有著不同的場景,并被認為是對編程語言的一種強大的擴展。本質上,函數(shù)式接口也是接口,不過包含一個抽象的方法聲明。Java 標準庫中的Runnable接口就是這種理念的絕佳范例:
@FunctionalInterface public interface Runnable { void run(); }
Java 編譯器在處理函數(shù)式接口時有所不同,并能把lamdba表達式轉化為函數(shù)式接口的實現(xiàn)。我們先看一下下面方法的定義:
public void runMe( final Runnable r ) { r.run(); }
在Java 7及以前的版本中,必須要提供Runnable接口的具體實現(xiàn)(例如使用匿名類),但在Java 8中卻可以通過傳遞lambda表達式來運行run()方法:
runMe( () -> System.out.println( "Run!" ) );
最后,可以使用@FunctionalInterfact注解(注解會在枚舉和注解的使用章節(jié)進行詳細介紹)告知編譯器以在編譯階段驗證函數(shù)式接口中僅包含了一個抽象方法聲明,從而保證未來任何變更的引入不會破壞該接口的函數(shù)式特性。
抽象類抽象類是Java 語言支持的另外一個有趣的主題。抽象類與Java 7中的接口有些類似,與Java 8中支持默認方法的接口更為相像。不同于普通類,抽象類不能實例化,但可以被繼承。更重要的是,抽象類能包含抽象方法:一種沒有定義實現(xiàn)的特殊方法,類似于接口中的方法聲明,如下:
package com.javacodegeeks.advanced.design; public abstract class SimpleAbstractClass { public void performAction() { // Implementation here } public abstract void performAnotherAction(); }
在上述例子中,SimpleAbstractClass類被聲明為abstract,并且包含了一個abstract方法。當類有部分實現(xiàn)可被子類共享時,抽象類就變得特別有用,因為它還為子類對抽象方法的定制實現(xiàn)提供了支持入口。
另外,抽象類與接口還有一點不同在于接口只能提供public的聲明,而抽象類可使用所有的訪問控制規(guī)則來支持方法的可見性。
不可變類不可變性在現(xiàn)代軟件開發(fā)中的地位日益顯著。隨著多核系統(tǒng)的發(fā)展,隨之而來引入了大量并發(fā)與數(shù)據(jù)共享的問題(并發(fā)最佳實踐中會詳細介紹并發(fā)相關主題)。但有一個理念是明確的:系統(tǒng)的可變狀態(tài)越少(甚至不可變),擴展性和可維護性就越高。
遺憾的是,Java并未從語言特性上提供強大的不可變性支持。盡管如此,使用一些開發(fā)技巧依然能設計出不可變的類和系統(tǒng)。首先要保證類的所有字段均設置為final,當然這只是一個好的開始,你并不能單純的通過final就完全保證不可變性:
package com.javacodegeeks.advanced.design; import java.util.Collection; public class ImmutableClass { private final long id; private final String[] arrayOfStrings; private final Collection< String > collectionOfString; }
其次,遵循良好的初始化規(guī)則:如果字段聲明的是集合或數(shù)組,不要直接通過構造方法的參數(shù)進行賦值,而是使用數(shù)據(jù)復制,從而保證集合或數(shù)組的狀態(tài)不受外界的變化而改變:
public ImmutableClass( final long id, final String[] arrayOfStrings, final Collection< String > collectionOfString) { this.id = id; this.arrayOfStrings = Arrays.copyOf( arrayOfStrings, arrayOfStrings.length ); this.collectionOfString = new ArrayList<>( collectionOfString ); }
最后,提供合適的數(shù)據(jù)獲取手段(getters)。對于集合數(shù)據(jù), 應該使用Collections.unmodifiableXxx獲取集合的不可變視圖:
public CollectiongetCollectionOfString() { return Collections.unmodifiableCollection( collectionOfString ); }
對于數(shù)組,唯一能保證不可變性的方式只有逐一復制數(shù)組中的元素到新數(shù)組而不是直接返回原數(shù)組的引用。不過有些時候這種做法可能不切實際,因為過大的數(shù)組復制將會為增加垃圾回收的開銷。
public String[] getArrayOfStrings() { return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length ); }
盡管上面的例子提供了一些示范,然而不可變性依然不是Java中的一等公民。當不可變類的字段引用了其他類的實例時,情況可能會變得更加復雜。其他類也應該保證不可變,然而并沒有簡單有效的途徑進行保證。
有一些優(yōu)秀的Java源碼分析工具,像FindBugs 和 PMD能幫助你分析代碼并找出常見的Java代碼編寫缺陷。對于任何一個程序員,這些工具都應當成為你的好幫手。
匿名類在Java 8之前,匿名類是實現(xiàn)在類定義的地方一并完成實例化的唯一方式。匿名類的目的是減少不必要的格式代碼,并以簡捷的方式把類表示為表達式。下面看下Java中典型的創(chuàng)建線程的方式:
package com.javacodegeeks.advanced.design; public class AnonymousClass { public static void main( String[] args ) { new Thread( // Example of creating anonymous class which implements // Runnable interface new Runnable() { @Override public void run() { // Implementation here } } ).start(); } }
在上例中,需要Runnable接口的地方使用了匿名類的實例。盡管使用匿名類時有一些限制,然而其最大的缺點在于Java 語法強加給的煩雜語法。即便實現(xiàn)一個最簡單的匿名類,每次也都需要至少5行代碼來完成:
new Runnable() { @Override public void run() { } }
好在 Java 8中的lambda表達式和函數(shù)式接口消除了這些語法上的固有代碼,使得Java代碼可以變的更酷:
package com.javacodegeeks.advanced.design; public class AnonymousClass { public static void main( String[] args ) { new Thread( () -> { /* Implementation here */ } ).start(); } }可見性
我們在對象的創(chuàng)建與銷毀章節(jié)中已經(jīng)學習過Java中的可見性與可訪問性的概念,本部分我們回過頭看看父類中定義的訪問修飾符在子類里的可見性:
修飾符 | 包可見性 | 子類可見性 | 公開可見性 |
---|---|---|---|
public | 可見 | 可見 | 可見 |
protected | 可見 | 可見 | 不可見 |
<無修飾符> | 可見 | 不可見 | 不可見 |
private | 不可見 | 不可見 | 不可見 |
表 1
不同的可見性級別限制了類或接口對其他類(例如不同的包或嵌套的包中的類)的可見性,也控制著子類對父類中定義的方法、函數(shù)方法及字段的可見與可訪問性。
在接下面的繼承,我們會看到父類中的定義對子類的可見性。
繼承繼承是面向對象編程的核心概念之一,也是構造類的關系的基礎。憑借著類的可見性與可訪問性規(guī)則,通過繼承可實現(xiàn)易擴展和維護的類層次關系。
語法上,Java 中實現(xiàn)繼承的方式是通過extends關鍵字后跟著父類名實現(xiàn)的。子類從父類中繼承所有public和protected的成員,如果子類與父類處于同一個包中,子類也將會繼承只有包訪問權限的成員。不過話說回來,在設計類時,應保持具有最少的公開方法或能被子類繼承的方法。下面通過Parent類和Child類來說明不同的可見性及達到的效果:
package com.javacodegeeks.advanced.design; public class Parent { // Everyone can see it public static final String CONSTANT = "Constant"; // No one can access it private String privateField; // Only subclasses can access it protected String protectedField; // No one can see it private class PrivateClass { } // Only visible to subclasses protected interface ProtectedInterface { } // Everyone can call it public void publicAction() { } // Only subclass can call it protected void protectedAction() { } // No one can call it private void privateAction() { } // Only subclasses in the same package can call it void packageAction() { } }
package com.javacodegeeks.advanced.design; // Resides in the same package as parent class public class Child extends Parent implements Parent.ProtectedInterface { @Override protected void protectedAction() { // Calls parent"s method implementation super.protectedAction(); } @Override void packageAction() { // Do nothing, no call to parent"s method implementation } public void childAction() { this.protectedField = "value"; } }
繼承本身就是一個龐大的主題, 在Java語言中也制定了一系列精細的規(guī)范。盡管如此,還是有一些易于遵循的原則幫助你實現(xiàn)精練的類層次結構。在Java中,子類可以重載從父類中繼承過來的任意非final方法(final的概念參見Final類和方法)。
然而,起初在Java中并沒有特定的語法或關鍵字標識方法是否是重載了的,這常常會給代碼的編寫引入混淆。因此后來引入了@Override注解用于解決這個問題:當你確實是在重載繼承來的方法時,請使用@Override注解進行標記。
另外一個Java開發(fā)者經(jīng)常需要權衡的問題在設計系統(tǒng)時使用類繼承(具體類或抽象類)還是接口實現(xiàn)。這個建議就是優(yōu)先選擇接口實現(xiàn)而非繼承。因為接口更為輕量,易于測試(通過接口mock)和維護,并能降低修改實現(xiàn)所帶來的副作用。很多優(yōu)秀的編程技術都偏向于依賴接口為標準Java庫創(chuàng)建代理。
多重繼承不同于C++或其他編程語言,Java并不支持多重繼承:Java中的每個類最多只能有一個直接的父類(在使用Object的通用方法中我們知道Object類處于繼承層次的頂端)。然而Java中的類可以實現(xiàn)多個接口,所以實現(xiàn)多個接口是Java中達到多重繼承效果的唯一途徑。
package com.javacodegeeks.advanced.design; public class MultipleInterfaces implements Runnable, AutoCloseable { @Override public void run() { // Some implementation here } @Override public void close() throws Exception { // Some implementation here } }
盡管實現(xiàn)多個接口的方式非常強大,但有時為了更好的重用某個接口的實現(xiàn),你不得不通過更深的類繼承層次以達到多重繼承的效果:
public class A implements Runnable { @Override public void run() { // Some implementation here } }
// Class B wants to inherit the implementation of run() method from class A. public class B extends A implements AutoCloseable { @Override public void close() throws Exception { // Some implementation here } }
// Class C wants to inherit the implementation of run() method from class A // and the implementation of close() method from class B. public class C extends B implements Readable { @Override public int read(java.nio.CharBuffer cb) throws IOException { // Some implementation here } }
Java中引入的默認方法在一定程序上解決了類繼承層次過深的問題。隨著默認方法的引入,接口便不只是提供方法聲明約束,同時還可以提供默認的方法實現(xiàn)。相應的,實現(xiàn)了此接口的類也順帶著繼承了接口中實現(xiàn)的方法。示例如下:
package com.javacodegeeks.advanced.design; public interface DefaultMethods extends Runnable, AutoCloseable { @Override default void run() { // Some implementation here } @Override default void close() throws Exception { // Some implementation here } } // Class C inherits the implementation of run() and close() methods from the // DefaultMethods interface. public class C implements DefaultMethods, Readable { @Override public int read(java.nio.CharBuffer cb) throws IOException { // Some implementation here } }
多重繼承雖然很強大,卻也是個危險的工具。眾所周知的"死亡鉆石(Diamond of Death)"就常作為多重繼承的主要缺陷被提及,所以開發(fā)者在設計類繼承關系時務必多加小心。湊巧Java 8接口規(guī)范里的默認方法也同樣成了死亡鉆石的犧牲品。
interface A { default void performAction() { } } interface B extends A { @Override default void performAction() { } } interface C extends A { @Override default void performAction() { } }
根據(jù)上面的定義,下面的接口E將會編譯失敗:
// E is not compilable unless it overrides performAction() as well interface E extends B, C { }
坦白的說,作為面向對象編程語言,Java一向都在盡力避免一些極端場景。然而避免語言本身的發(fā)展,這些極端場景也逐漸暴露。
繼承與組合好在繼承并非設計類的關系的唯一方式。組合是另外一種被大多開發(fā)者認為優(yōu)于繼承的設計方法。其主旨也相當簡單:取代層次結構,類應該由其他類組合而來。
先看一個簡單的例子:
public class Vehicle { private Engine engine; private Wheels[] wheels; // ... }
Vehicle類由engine和wheels組成(簡單起見,忽略了其他的組成部分)。
不過也有人會說Vehicle也是一個engine,因此應該使用繼承的方式:
public class Vehicle extends Engine { private Wheels[] wheels; // ... }
到底哪種設計才是正確的呢?業(yè)界通用的原則分別稱之為IS-A和HAS-A規(guī)則。IS-A代表的是繼承關系:子類滿足父類的規(guī)則,從而是父類的一個(IS-A)變量。與之相反,HAS-A代表的是組合關系:類擁有(HAS-A)屬于它的對象。通常,HAS-A優(yōu)于IS-A,原因如下:
設計更靈活,便于以后的變更
模型更穩(wěn)定,變化不會隨著繼承關系擴散
依賴更松散,而繼承把父類與子類緊緊的綁在了一起
代碼更易讀,類所有的依賴都被包含在類的成員聲明里
盡管如此,繼承也有自己的用武之地,在解決問題時不應被忽略。在設計面向對象模型時,要時刻記著組合和繼承這兩種設計方法,盡可能多些嘗試以做出最優(yōu)選擇。
封裝在面向對象編程中,封裝的含義就是把細節(jié)(像狀態(tài)、內部方法等)隱藏于內部而不暴露于實現(xiàn)之外。封裝帶來的好處就是提高了可維護性,并便于將來的變更。越少的細節(jié)暴露,就會帶來越多的未來變更實現(xiàn)的控制權,而不用擔心破壞其他代碼(如果你是一位代碼庫或框架的開發(fā)者,一定會遇到這種情景)。
在Java語言中,封裝是通過可見性和可訪問性規(guī)則實現(xiàn)的。公認的優(yōu)秀實踐就是從不直接暴露類的字段,而是通過setter(如果字段沒有聲明為final)和getter的方式來訪問它。請看下面的例子:
package com.javacodegeeks.advanced.design; public class Encapsulation { private final String email; private String address; public Encapsulation( final String email ) { this.email = email; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getEmail() { return email; } }
類似例子中定義的類,在Java中稱作JavaBeans:遵循"只能以setter和getter方法的方式暴露允許外部方法的字段"規(guī)則的普通Java類。
如我們在繼承部分強調過的,遵守封裝原則,把公開的信息最小化。不需要public的時候, 要使用private替代(或者protected/package/private,取決于具體的問題場景)。在將來的維護過程,你會得到回報:帶給你足夠的自由來優(yōu)化設計而不會引入破壞性的變更(或者說對外部的變更達到最小化)。
Final 類和方法在Java中,有一種方式能阻止類被其他類繼承:把類聲明為final。
package com.javacodegeeks.advanced.design; public final class FinalClass { }
final修飾在方法上時,也能達到阻止方法被重載的效果:
package com.javacodegeeks.advanced.design; public class FinalMethod { public final void performAction() { } }
是否應該使用final修飾類或方法并無定論。Final的類和方法一定程度上會限制擴展性,并且在設計之初很難判斷類是否會被繼承、方法是否能被重載。對于類庫開發(fā)者,尤其值得注意,使用final可能會嚴重影響類庫的適用性。
Java標準庫中有一些final類的例子,例如眾所周知的String類。在很早時候,就把String設計成了final,從而避免了開發(fā)者們自行設計的好壞不一的字符串實現(xiàn)。
源碼下載可以從這里下載本文中的源碼:advanced-java-part-3
下章概要在本章節(jié)中,我們學習了Java中的面向對象設計的概念。同時簡單介紹了基于契約的開發(fā)方式,涉及了一些函數(shù)式編程概念,也看到了編程語言隨著時間的演進。在下一章中,我們將會學習到泛型編程,以及如何實現(xiàn)類型安全的編程。
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/65487.html
摘要:很多情況下,通常一個人類,即創(chuàng)建了一個具體的對象。對象就是數(shù)據(jù),對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 馬上就要到七夕了,離年底老媽老爸...
摘要:很多情況下,通常一個人類,即創(chuàng)建了一個具體的對象。對象就是數(shù)據(jù),對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 馬上就要到七夕了,離年底老媽老爸...
摘要:很多情況下,通常一個人類,即創(chuàng)建了一個具體的對象。對象就是數(shù)據(jù),對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 馬上就要到七夕了,離年底老媽老爸...
摘要:責任鏈模式的具體運用以及原理請參見筆者責任鏈模式改進方式引入適配器模式關于接口適配器模式原理以及使用場景請參見筆者適配器模式。 1 責任鏈模式現(xiàn)存缺點 由于責任鏈大多數(shù)都是不純的情況,本案例中,只要校驗失敗就直接返回,不繼續(xù)處理接下去責任鏈中的其他校驗邏輯了,故而出現(xiàn)如果某個部分邏輯是要由多個校驗器組成一個整理的校驗邏輯的話,則此責任鏈模式則顯現(xiàn)出了它的不足之處了。(責任鏈模式的具體運...
閱讀 3610·2021-11-23 09:51
閱讀 1482·2021-11-04 16:08
閱讀 3554·2021-09-02 09:54
閱讀 3620·2019-08-30 15:55
閱讀 2601·2019-08-30 15:54
閱讀 963·2019-08-29 16:30
閱讀 2051·2019-08-29 16:15
閱讀 2322·2019-08-29 14:05