Java 面向对象细节
文章目錄
- 前言
- 二、類與對象
- 2.0、類的定義與對象的使用
- 2.0.1、類的語法格式
- 2.0.2、對象的創建和使用
- 2.0.3、Java堆內存與棧內存
- 2.0.4、this引用
- 2.1、static修飾符
- 2.2、Java方法
- 2.2.1、方法重載overload
- 2.2.2、方法重寫override
- 2.3、Java變量
- 2.4、訪問控制符private default protected public
- 2.5、類的設計
- 2.6、包裝類
- 2.7、toString()
- 2.7、"=="和equals()
- 2.7.1、"=="
- 2.7.2、equals()
- 2.8、單例類(Singleton類)
- 2.9、final修飾符
- 2.9.1、final成員變量
- 2.9.2、final局部變量
- 2.9.3、final基本類型變量和引用類型變量
- 2.9.4、可執行“宏替換”的final變量
- 2.9.5、final 方法
- 2.9.6、final 類和不可變類
- 2.10、抽象類
- 2.11、接口
- 2.11.1、接口概述
- 2.11.2、接口的繼承
- 2.11.3、接口和抽象類
- 2.11.4、面向接口編程
- 2.12、內部類
- 2.12.1、非靜態內部類
- 2.12.2、靜態內部類
- 2.12.3、使用內部類
- 2.12.4、局部內部類
- 2.12.5、匿名內部類
- 2.13、Lambda表達式與函數式接口使用簡介
- 2.14、枚舉類簡介
- 2.15、對象和垃圾回收
- 總結
前言
??????Java是面向對象的程序設計語言,類是面向對象的重要內容,可以把類當成一種自定義類型,可以使用類來定義變量,這種類型的變量統稱為引用變量,也就是說,所有類是引用類型。
二、類與對象
2.0、類的定義與對象的使用
2.0.1、類的語法格式
??????類中static修飾的成員不能訪問沒有static修飾的成員。
??????定義類的語法格式: 【修飾符】 class 類名
??????定義構造器的語法格式: 【修飾符】 構造器名 (形參列表)
??????定義成員變量的語法格式: 【修飾符】 類型 成員變量名 【=默認值】
??????定義方法的語法格式: 【修飾符】 方法返回值類型 方法名 (形參列表)
??????修飾符可以省略,也可以是public、private、protected、static、final。其中public、private、protected三個最多只能出現一個,但可以與static、final組合起來修飾成員變量。
??????構造器是一個類創建對象的根本途徑,構造器名必須和類名相同,構造器不需要定義返回值類型。如果程序員沒有為一個類編寫構造器,則系統會為該類提供一個默認的構造器。一旦程序員為一個類提供了構造器,系統將不再為該類提供構造器。
??????field:成員變量、字段或域。屬性(property)在Java中指的是setter和getter方法。比如說某個類具有age屬性,意味著該類包括setAge()和getAge()兩個方法??梢詤⒄障聢D:
2.0.2、對象的創建和使用
??????創建對象的根本途徑是構造器,通過 new 關鍵字來調用某個類的構造器即可創建這個類的實例。如果訪問權限允許,類里定義的方法和成員變量都可以通過類或實例來調用。類或實例訪問方法成員變量的語法是:類.類變量|方法,實例.實例變量|方法,其中類或實例是主調者,用于訪問該類或該實例的成員變量或方法。
??????大部分情況下,定義一個類就是為了重復創建該類的實例,同一個類的多個實例具有相同的特征,而類則是定義了多個實例的共同特征。因此類不是一種具體存在,實例才是具體存在。
2.0.3、Java堆內存與棧內存
??????當一個方法執行時,每個方法都會建立自己的內存棧,在這個方法內定義的變量將會逐個放入這塊棧內存里,隨著方法的執行結束,這個方法的內存棧也將自然銷毀。因此,所有在方法中定義的局部變量都是放在棧內存中的(包括基本類型變量和對象引用變量);而在程序中創建一個對象時:這個對象本身將被保存到運行時數據區中,以便重復利用(因為對象的創建成本通常較大),這個運行時數據區就是堆內存。堆內存中的對象不會隨方法的結束而銷毀,即使在方法結束后,這個對象還可能被另一個引用變量所引用(在方法的參數傳遞時很常見)。只有當一個對象沒有任何引用變量引用它時,系統的垃圾回收器才會在合適的時候回收它。
??????如果堆內存中的數組不再有任何變量指向自己,則這個數組將成為垃圾,該數組所占的內存將會被GC回收。因此,為了讓垃圾回收機制回收一個數組所占的內存空間,可以將數組變量賦為null,也就切斷了數組引用變量和實際數組之間的引用關系,實際的數組也就成了垃圾。這個道理對于對象也是同樣適用的,還可以調用Runtime對象的 gc()或 System.gc()等方法來建議系統進行垃圾回收(但也不能確定系統立即會進行垃圾回收)。
??????假設 Person 為一個類,對于語句:Person p = new Person(); 這個語句產生了兩個東西,它們占用了兩塊不同的內存,一塊在棧內存中,另一塊在堆內存中。變量 p 的類型是 Person,它是一個引用數據類型,它被放在棧內存中;而由 p 指向的剛剛創建的 Person 類型實例(new Person())被放在了堆內存中。類似于 c 語言中的指針,p 中封裝的是新創建對象的首地址,所以只需要通過操作符 . 來訪問對象的實例變量和方法就好了。
2.0.4、this引用
??????this關鍵字總是指向調用該方法的對象。this 的最大作用就是讓類中的一個方法,訪問該類中的另一個方法或實例變量。通常情況下可以省略 this 前綴,但實際上這個 this 仍然是存在的。
2.1、static修飾符
??????關鍵字static:用它修飾的方法或成員變量,表明該成員屬于這個類本身,而不屬于該類的單個實例,故將static修飾的成員變量或方法稱為類變量、類方法。 不用static修飾的成員屬于該類的單個實例,而不屬于該類。故將不使用static修飾的成員變量或方法稱為實例變量、實例方法(Instance 方法)。
??????靜態變量的其它說明:①隨著類的加載而加載,可通過“類.靜態變量”的方式進行調用。②早于對象的創建。③由于類只會加載一次,則靜態變量在內存中只有一份
??????實例變量:當創建了類的多個對象時,每個對象都獨立的擁有一套類中的非靜態屬性。當修改其中某個對象的非靜態屬性時,不會導致其他對象中同樣的屬性值的修改。
??????靜態變量:當創建了類的多個對象時,多個對象共享同一個靜態變量。當通過某個對象修改靜態變量時,會導致其他對象調用此靜態變量時,也發生變化。
??????如果一個屬性是需要被多個對象所共享的,不會因為對象的不同而發生變化的,可以設置其為靜態屬性。
??????簡單的來講:如果一個方法僅需要訪問靜態屬性變量或方法,那么推薦將其設置為靜態方法。也需要在具體場景下具體分析。
??????由于static有靜態的意思,也將static修飾的成員變量或方法稱為靜態變量、靜態方法。將不使用static修飾的成員變量或方法稱為非靜態變量、非靜態方法。 靜態成員不能直接訪問非靜態成員。
2.2、Java方法
??????Java的方法不能獨立存在,它必須屬于一個類或一個對象,且不能像函數那樣獨立執行,執行方法時必須使用類或對象來作為調用者,即所有方法都必須使用“類.方法”或“對象.方法”來實現。在同一個類里不同方法之間相互調用時,如果被調用的方法是普通方法,則默認采用this作為調用者(調用方法通過this引用自己當前所在的對象);如果被調用的方法是靜態方法,則默認采用類作為調用者。
??????方法的參數傳遞機制:Java的實參傳入方法的方式只有一種,即值傳遞。所謂值傳遞,就是將實際參數值的副本(復制品)傳入方法內,而參數本身不會受到影響。
2.2.1、方法重載overload
??????Java 允許同一個類中定義多個同名方法,只要形參列表不同就行。如果一個類中包含了兩個或兩個以上方法的方法名相同,但形參列表不同,則被稱為方法重載。確定一個方法需要三個要素:1,調用者 2,方法名 3,形參列表,這三要素也構成了方法簽名(signature)。方法重載即:同一個類中方法名相同,但是參數列表不同。
2.2.2、方法重寫override
??????當子類在繼承父類時,可以定義一些新的特征,以及修改父類的方法,也被稱為對父類方法的覆蓋。
??????所謂方法的重寫是指子類中的方法與父類中繼承的方法有完全相同的返回值類型、方法名、參數個數以及參數類型。
??????如果子類將父類中的方法重寫了,調用的時候肯定是調用被重寫過的方法,那么如果現在一定要調用父類中的方法該怎么辦呢?此時,通過使用super關鍵就可以實現這個功能,super關鍵字可以從子類訪問父類中的內容,如果要訪問被重寫過的方法,使用“super.方法名(參數列表)”的形式調用。
2.3、Java變量
??????類的成員變量初始化:一般來講,無須顯式初始化,只要為一個類定義了類變量或實例變量,系統就會在這個類的準備階段或創建該類的實例時進行默認初始化,默認初始化基本類型為0,引用類型為null。 而局部變量必須初始化之后才能使用。
??????關于成員變量的使用規則:如果某個變量表示的是這個類的固有信息,應采用類變量,如果表示的是某個實例的固有信息,應采用實例變量。比如:對于Person類來說,眼睛的個數應設置為類變量,因為所有實例都具有兩只眼睛。而身高體重信息應該設置為實例變量,因為每個實例的身高體重信息不盡相同。
??????關于局部變量的使用規則:應該在程序中盡可能地縮小局部變量的作用范圍,局部變量的作用范圍越小,它在內存中停留的時間就越短,程序運行性能就越好。因此,能使用代碼塊局部變量的地方,就不要使用方法局部變量。(這點目前意識的不是很清楚)
??????當Java創建一個對象時,系統先為該對象的所有實例變量分配內存(前提是該類已經被加載過了),接著程序開始對這些實例變量執行初始化,初始化順序為:先執行初始化塊或聲明實例變量時指定的初始值(這兩個地方指定初始值的執行允許與它們在源代碼中的排列順序相同),再執行構造器里指定的初始值。
2.4、訪問控制符private default protected public
??????訪問控制符級別:private < default < protected < public 。其中default為不加任何訪問控制符的訪問控制級別。private(當前類訪問權限):用它修飾的成員只能在當前類的內部被訪問。default(包訪問權限):用它修飾的成員或外部類可以被相同包下面的其他類訪問。protected(子類訪問權限):用它修飾的成員既可以被同一個包中的其他類訪問,也可以被不同包中的子類訪問。通常采用protected修飾一個方法時,是希望其子類來重寫該方法的。public(公共訪問權限):用它修飾的成員就可以被所有類訪問,不管訪問類與被訪問類是否處于同一個包中,是否具有父子繼承關系。
??????外部類的訪問級別通常有兩種:public和默認(default)。使用public來修飾即表示該類可以被所有類使用,使用default來修飾表示該類只能被同一個包中的其他類使用。
??????如果一個Java源文件里定義的所有類都沒有使用public修飾,則這個Java源文件的文件名可以是一切合法的文件名;但如果一個Java源文件里定義了一個public修飾的類,則這個源文件的文件名必須與public修飾的類的類名相同。
??????Java默認為所有源文件導入 java.lang包下的所有類,包括System和String等等類。
??????在不使用 import 的情況下:創建一個 HashMap 的語句為: java.util.Map a =new java.util.HashMap<>();
2.5、類的設計
??????高內聚低耦合,是軟件工程中的概念,是判斷軟件設計好壞的標準,主要用于程序的面向對象的設計,主要看類的內聚性是否高,耦合度是否低。目的是使程序模塊的可重用性、移植性大大增強。通常程序結構中各模塊的內聚程度越高,模塊間的耦合程度就越低。內聚是從功能角度來度量模塊內的聯系,一個好的內聚模塊應當恰好做一件事,它描述的是模塊內的功能聯系;耦合是軟件結構中各模塊之間相互連接的一種度量,耦合強弱取決于模塊間接口的復雜程度、進入或訪問一個模塊的點以及通過接口的數據。一個類常常就是一個小的模塊,應該只讓這個模塊公開必須讓外界知道的內容,而隱藏其他一切內容。進行程序設計時,應盡量避免一個模塊直接操作和訪問另一個模塊的數據 ,模塊設計追求高內聚(盡可能把模塊的內部數據、功能實現細節隱藏在模塊內部獨立完成,不允許外部直接干預)、低耦合(僅暴露少量的方法給外部使用)。
2.6、包裝類
??????包裝類實現基本類型與字符串之間的轉換:
??????將字符串類型的值轉換為基本類型的值:1,利用包裝類提供的parseXxx(String s)靜態方法;2,利用包裝類提供的valueOf(String s)靜態方法 ??????例如:int i1=Integer.parseInt(“1234”); ?????? int i2=Integer.valueOf(“1234”);
??????將基本類型的值轉換為字符串:1,采用String類的多個重載valueOf()方法 ??????例如:String s=String.valueOf(234 / 3.14f / true);2,將基本類型變量與空字符串"“進行拼接,系統會自動將基本類型變量轉換為字符串。??????例如:String s=5 + “” ;結果s的值為"5” 。
2.7、toString()
??????toString() 方法默認返回一個對象實現類的“類名 + @ + hashCode”
2.7、"=="和equals()
2.7.1、"=="
??????"=="表示兩個引用類型變量指向同一個對象,也就是這兩個引用類型變量(指針)的值是否相等;對于兩個基本類型變量,且都是數值類型(不一定要求數值類型嚴格相同),則只要兩個變量的值相等,就返回true。
??????例如:String str1 = new String(“hello”); String str2 = new String(“hello”) 如果用"==“判斷 str1 和 str2 是否相等時,返回false。(new String(“hello”)語句執行時,JVM會先使用常量池來管理“hello”直接量,再調用String類的構造器來創建一個新的String對象,新創建的String對象被保存在堆內存中。這是“hello”與new String(“hello”)的區別。)
??????int it=65; float f1=65.0f; char ch=‘A’; 這三者用”=="來判斷都會返回true。
2.7.2、equals()
??????Object類提供的實例方法equals()用來判斷兩個對象是否相等與采用"==’'沒有區別。故其沒有太大的實際意義,如果希望采用自定義的相等標準,則可重寫equals方法來實現。
??????而String類已經重寫了equals方法,只要兩個字符串所包含的字符序列相同,通過equals方法比較將返回true,否則返回false。
??????重寫Object類的equals方法示例: 假設當前類為 Person 類,成員變量為String類型的 name 和String類型的 idstr 。
public boolean equals(Object obj){//如果兩個對象為同一個對象if(this==obj)return true;//只有當obj為Person對象時if(obj!=null&&obj.getClass()==Person.class){Person personObj=(Person)obj;//自定義標準只要兩個對象的Stirng變量idstr值相等即可。if(this.getIdstr().equals(personObj.getIdstr()))return ture;}return false;}??????說明:上面程序判斷 obj 是否為 Person 類的實例時,如果采用 instanceof 來判斷的話,當前面的對象是后面類的實例或其子類的實例都將返回 true。而如果是 obj 是 Person 的子類的話,采用 instanceof 也會返回 true,而對父類對象和子類對象來重載 equals 方法判斷是否相等沒有太大意義,一般情況下,我們的目的是對同一個類的兩個對象(不能包含子類)來比較。所以改為使用 obj.getClass()==Person.class 比較合適。
2.8、單例類(Singleton類)
??????如果一個類只能創建一個實例,則這個類被稱為單例類。在一些特殊的場景下:要求不允許自由創建該類的對象,而只允許為該類創建一個對象。所以需要采用 private 來修飾該類的構造器。
??????根據封裝的原則:當構造器被隱藏起來的話,就需要提供一個 public 方法作為該類的訪問點,用于創建該類的對象,并且該方法必須使用 static 修飾(因為調用該方法之前還不存在對象,因此調用該方法的不可能是對象,只能是類)。
??????當然,該類還需要存儲已經創建的對象,用來確保只能創建一個對象。為此該類需要使用一個成員變量來保存曾經創建的對象,因為該成員變量需要被上面的靜態方法訪問,故該成員變量必須使用 static 修飾。
class Singleton{private static Singleton instance;//存儲曾經創建的實例。private Singleton(){};//隱藏構造器方法//getInstance方法用來返回一個Singleton實例//并且方法實現里加入自定義控制,用來保證只產生一個Singleton對象public static Singleton getInstance(){if(instance==null)instance=new Singleton();return instance;} } public class SingletonTest {public static void main(String[] args) {Singleton s1=Singleton.getInstance();Singleton s2=Singleton.getInstance();System.out.println(s1==s2);//結果為true} }2.9、final修飾符
??????final 用來修飾類、變量和方法,類似于C#里的 sealed 關鍵字,用來表示它修飾的類、方法和變量不可改變。final 修飾的變量一旦獲得了初始值,該 final 變量的值就不能被重新賦值。
2.9.1、final成員變量
??????final 修飾的成員變量必須由程序員顯式地指定初始值。否則這些成員變量的值就會一直為系統默認分配的 0、‘0’、false和null。
??????final類變量:必須在靜態初始化塊或聲明該變量時指定初始值,二者之中選其一。
??????final實例變量:必須在普通初始化塊、聲明該實例變量時或構造器中指定初始值,三者之中選其一。
2.9.2、final局部變量
??????final 局部變量在定義時可以指定默認值,或不指定默認值。指定默認值的在后面代碼中就不能再賦值了;在不指定默認值的情況下,可以在后面代碼中對該 final 變量賦初始值,但只能一次,不能再次賦值。
2.9.3、final基本類型變量和引用類型變量
??????但使用 final 來修飾基本類型變量時,不能對基本類型變量重新賦值,因此基本類型變量不能被改變。但是對于引用類型變量而言,它保存的僅僅是一個引用,final 只保證這個引用類型變量所引用的地址不會改變,即保證一直引用同一個對象,但是這個對象本身是可以被改變的。
2.9.4、可執行“宏替換”的final變量
??????對于一個 final 變量來說,不管它是類變量、實例變量或局部變量,只要其滿足三個條件:使用 final 修飾符修飾;在定義該 final 變量時指定了初始值;并且該初始值可以在編譯時就被確定下來。
??????如果代碼有語句:final int a = 5; System.out.println(a); 那么在此處打印的變量 a 其實根本不存在,當程序執行時,直接將其轉換為 System.out.println(5); ??????即在此處的變量 a 就相當于一個宏變量,編譯器會把程序中所有用到該變量的地方直接替換為該變量的值。類似于 #define a 5
2.9.5、final 方法
??????final 修飾的方法不能被重寫,當不希望子類重寫父類的某個方法時,可以使用 final 來修飾。在 Java 的 Object 類中存在一個 final 方法:getClass(),因為 Java 不希望任何類重寫該方法。相反對于 toString() 和 equals() 方法,是允許子類重寫的。
2.9.6、final 類和不可變類
??????final 修飾的類不能有子類。不可變類是指創建該類的實例后,該實例的實例變量是不可改變的。Java提供的8個包裝類和 java.lang.String類都是不可變類。在不可變類的內部就是采用 final 修飾的成員變量,導致成員變量只要被初始化一次之后,就不能再被改變。
2.10、抽象類
??????存在抽象方法的類只能被定義為抽象類,但是抽象類里可以沒有抽象方法。抽象方法和抽象類用 abstract 來修飾。
??????抽象方法:在父類中只有方法簽名,沒有方法體,例如:對于方法 public void test () { } 來說,對應的抽象方法格式為 public abstract void test (); ,抽象方法意味著這個方法必須由子類提供實現(重寫)。
??????抽象類不能被實例化,只能當做父類被子類繼承。抽象類的構造器不能創建實例,主要用于被子類調用。
??????抽象類是從多個具體類中抽象出來的父類,它具有更高層次的抽象。抽象類體現的就是一種模板模式的設計,抽象類作為多個子類的通用模板,子類在抽象類的基礎上進行擴展、改造,但子類總體上會大致保留抽象類的行為方式。如果編寫一個抽象父類,父類提供了多個子類的通用方法,并把一個或多個方法留個其子類實現,這就是一種模板模式,模板模式是一種十分常見且簡單的設計模式之一。
??????模板模式:1,抽象父類可以只定義需要使用的某些方法,把不能實現的部分抽象成抽象方法,留給其子類去實現。2,父類中可能包含需要調用其他系列方法的方法,這些被調方法既可以由父類實現,也可以由其子類實現。父類里提供的方法只是定義了一個通用算法,其實現也許并不完全由自身實現,而必須依賴于其子類的輔助。
2.11、接口
2.11.1、接口概述
??????抽象類是從多個類中抽象出來的模板,如果將這種抽象進行得更徹底,則可以提煉出一種更加特殊的“抽象類” - 接口。接口定義的是多個類共同的公共行為規范,它不關心這些類的內部狀態數據,也不關心這些類里方法的實現細節,它只規定這批類必須提供某些方法,提供這些方法的類就可以滿足實際需要。接口體現的是規范和實現分離的設計思想,采用這種面向接口的耦合,會盡量降低各模塊之間的耦合,可以提供更好的可擴展性和可維護性。這些行為是與外部交流的通道,這就意味著接口里通常是定義一組公用方法,這組公用方法就是后面會提到的普通方法,也即抽象實例方法(public abstract)。
??????Java 9 中接口的定義格式:
??????【修飾符】 interface 接口名 extends 父接口1,父接口2…
??????{
????????????零個到多個常量定義…
????????????零個到多個抽象方法定義…
????????????零個到多個內部類、接口、枚舉定義…
????????????零個到多個私有方法、默認方法或類方法定義…
??????}
??????接口定義的是一組規范,所以接口里不能包含構造器和初始化塊定義。接口里可以包含成員變量(只能是靜態變量)、方法(只能是抽象實例方法,類方法,默認方法(Java 8)或私有方法(Java 9))、內部類(內部接口,枚舉)定義。
??????接口中的靜態常量默認采用 public static final 修飾符,不管定義時是否指定該修飾符,系統都會自動使用 public static final 來修飾。
??????接口中的內部類(內部接口,枚舉)默認采用 public static 修飾符,不管定義時是否指定該修飾符,系統都會自動使用 public static 來修飾。
??????在接口中定義的方法只有4種:抽象方法、類方法、默認方法(Java 8)和私有方法(Java 9),如果不是定義默認方法、類方法或私有方法,系統自動為普通方法增加 abstract 修飾符,即為抽象方法;
??????定義接口里的普通方法時,不管是否采用 public abstract 修飾符,接口里的普通方法總是使用 public abstract 來修飾。接口里的普通方法不能有方法實現(方法體);但類方法、默認方法、私有方法都必須有方法實現(方法體)。
??????具體的,見如下示例:
public interface Output {int MAX_CACHE_LINE=50; //成員變量只能是常量,即 public static final 類型的void out();//普通方法只能是 public 的抽象方法,即 public abstract 類型的。void getData(String msg);default void print(String... msgs)//默認方法需要采用 default 來修飾{for(String m:msgs)System.out.println(m);}default void test(){System.out.println("默認的 test() 方法");}static String staticTest()//接口中的類方法用 static 來修飾{return "這是一個類方法";}private void foo()// 私有方法用 private 來修飾, 注意:這是從JDK9開始才允許采用 private 修飾符的。 私有方法常用來作為工具方法{System.out.println("一個私有方法");}private static void bar(){System.out.println("一個私有靜態方法");} }??????在接口中的普通方法定義了這個接口的規范,如上例: 只要某個類具備了 取得數據(getData)或輸出數據(out) 的功能就認為它是一個實現了Output接口的設備,而不關心具體的實現細節。
??????在接口中的默認方法因為沒有用 static 來修飾,所以不能直接使用接口來調用默認方法,需要使用接口的實現類的實例來調用這些方法。(接口的默認方法可以認為就是一般意義上的實例方法)
??????在接口中的類方法可以直接調用接口來實現。
??????在接口中的私有方法的主要作用就是作為工具方法,為接口中的默認方法或類方法提供支持。(Java 9 更新的內容,為了解決當兩個默認方法(類方法)中包含一段相同的實現邏輯時, 需要將這段實現邏輯抽取成為一個工具方法,而這個工具方法應該是被隱藏的,所以就有了私有方法)
2.11.2、接口的繼承
??????接口的繼承和類繼承不一樣,接口完全支持多繼承,即一個接口可以有多個直接父接口。和類繼承相似的,子接口會獲得父接口里定義的所有抽象方法、常量。
??????繼承多個父接口的接口如下:
interface Product{int getProductTime(); } interface Output {int MAX_CACHE_LINE=50; //成員變量只能是靜態常量,即 public static final 類型的void out();//普通方法只能是 public 的抽象方法,即 public abstract 類型的。void getData(String msg);default void print(String... msgs)//默認方法需要采用 default 來修飾{for(String m:msgs)System.out.println(m);}default void test(){System.out.println("默認的 test() 方法");}static String staticTest()//接口中的類方法用 static 來修飾{return "這是一個類方法";} }public class Printer implements Product,Output {private ArrayDeque<String> printData=new ArrayDeque<>();public void out(){while(!printData.isEmpty()){System.out.println("Print: "+printData.pollFirst());}}public void getData(String msg) {if(printData.size()>=MAX_CACHE_LINE)System.out.println("FUll. can't get data.");elseprintData.offerLast(msg);}public int getProductTime(){return 5;}public static void main(String[] args){Output o=new Printer();o.getData("abc");o.getData("def");o.out();o.print("趙","錢","孫","李");// o 繼承了Output中的默認方法o.test();Product p =new Printer();System.out.println(p.getProductTime());} }2.11.3、接口和抽象類
??????接口作為系統與外界交互的窗口,接口體現的是一種規范。對于接口的實現者而言,接口規定了實現者必須向外提供哪些服務(通過其內的抽象方法);對于接口的調用者而言,接口規定了調用者可以調用哪些服務,以及如何來調用這些服務(通過其內的抽象方法)。當在一個程序中使用接口時,接口是多個模塊之間的耦合標準;當在多個應用程序之間使用接口時,接口是多個程序之間的通信標準。
??????從某種程度上來看,接口制定了系統各模塊應該遵循的標準,因此一個系統的接口不應該經常改變。一旦接口被改變,可能會導致系統中大部分類都需要改寫。
??????抽象類則不一樣,抽象類作為系統中多個子類的共同父類,它所體現的是一種模板式設計。抽象類作為多個子類的抽象父類,可以被當成系統實現過程中的中間產品,這個中間產品已經實現了系統的部分功能(那些已經提供實現的方法),還需要更進一步地完善。
2.11.4、面向接口編程
??????接口體現的是一種規范和實現分離的思想,充分利用接口可以極好地降低程序各模塊之間的耦合,從而提高系統的可擴展性和可維護性。所以,很多軟件架構設計理論都倡導“面向接口”編程,而不是面向實現類編程,希望通過面向接口編程來降低程序的耦合。
??????1,工廠模式:
??????假設程序中有一個 Computer 設備需要組合一個輸出設備,此時應該采用 Output 接口來組合還是采用 Printer 實現類呢?答案應該是采用 Output 接口,因為不能確保 Printer 實現類永遠不會迭代升級,相比之下,Output 接口的持久性要更好一些,并且組合一個 Output 接口可以將實現與規范相分離。示例如下:
??????2,命令模式:
??????如果某個方法需要完成某一種行為,但是這個行為的具體實現又無法確定,必須等到執行該方法時才能確定。這個時候也可以采用接口來實現。假設有個方法需要遍歷某個數組的數組元素,但無法確定在遍歷數組元素時如何處理這些元素,需要在調用該方法時指定具體的處理行為。
??????采用 Command 接口來定義一個方法,用這個方法來封裝“具體的處理行為”。示例如下:
public interface Command {//此方法定義的 process 方法用來封裝對數組元素的處理行為;void process(int element); }//定義具體的行為,其實完全可以將下面兩個實現放在兩個java文件中,而放在此僅為方便起見。 class CmdPrint implements Command{public void process(int element){System.out.println(element);} } class CmdAddOneAndPrint implements Command{public void process(int element){System.out.println(element+1);} } public class ProcessArray{//本類用來對數組進行處理,它采用了某種行為來對數組進行處理(由 Command 中的 process 方法來決定)。public void process(int[] target,Command cmd){for(int tmp:target)cmd.process(tmp);}public static void main(String[] args){int[] array={1,2,3,4,5,6};ProcessArray pa=new ProcessArray();Command cmdPrint=new CmdPrint();Command cmdAddOneAndPrint=new CmdAddOneAndPrint();pa.process(array,cmdPrint);pa.process(array,cmdAddOneAndPrint);} }??????故,接口體現的是規范和實現分離的設計思想,采用這種面向接口的耦合,會盡量降低各模塊之間的耦合,可以提供更好的可擴展性和可維護性。
2.12、內部類
??????當一個類被定義在另一個類的內部,這個定義在其他類內部的類就被稱為內部類,包含內部類的類也被稱為外部類。
??????內部類的作用:
1,內部類提供了更好的封裝,可以把內部類隱藏在外部類之內,不允許同一個包中的其他類訪問該類。
2,內部類被當為外部類的一個成員,同一個類的成員之間可以互相訪問,包括內部類成員可以直接訪問外部類的私有數據;但外部類不能訪問內部類的實現細節,例如內部類的實例成員(非靜態成員)。
3,匿名內部類適合創建那些僅需要一次使用的類。
??????外部類能使用的修飾符為 default 和 public。而內部類還可以使用 private、protected 和 static 。
??????外部類的上一級程序單元是包,所以它只有 2 個作用域:同一個包內或任何位置。因此只需要兩種訪問權限-包訪問權限和公開訪問權限。default(缺省)修飾符表示該類可以被同一個包下面的其他類來訪問。
??????內部類的上一級單元是外部類,所以它具有 4 個作用域:同一個類、同一個包、父子類和任何位置。(對應于: private、default、protected 和 public)而修飾符 static 則表示為靜態內部類。
??????大部分情況下,內部類都作為外部類的一個成員,與成員變量、方法、構造器和初始化塊相似。而局部內部類和匿名內部類則不是類成員。
2.12.1、非靜態內部類
??????成員內部類分兩種:靜態內部類(采用 static 修飾)和非靜態內部類(沒有采用 static 修飾)。非靜態內部類不能擁有靜態成員(靜態方法、靜態成員變量和靜態初始化塊)。
??????下例說明了:內部類中可以直接訪問外部類的成員,而外部類中卻不能直接訪問內部類的實例成員。其原因在于非靜態內部類對象存在時,外部類對象必然存在;而外部類對象存在時非靜態內部類對象不一定存在。
??????1,內部類如果存在變量名沖突,那么內部類成員變量采用 this.varName,外部類成員采用 外部類名.this.varName。
??????2,若要在外部類中訪問內部類的實例成員時,需要先創建一個內部類對象。
public class Cow {private double weight;public Cow(){};public Cow(double weight){this.weight=weight;}private class CowLeg{private double length;private String color;private double weight;public CowLeg(){};public CowLeg(double length,String color){this.length=length;this.color=color;}public void info(){double weight=0;System.out.println("color: "+ color+",length: "+length);System.out.println("weight of local: "+weight);System.out.println("weight of leg: "+this.weight);System.out.println("weight of cow: "+Cow.this.weight);//內部類可以直接訪問外部類的實例成員(包括私有成員變量)}}public void test(){//length=8.9; 外部類方法中不能直接訪問內部類的實例成員(即任何非靜態成員)//info(); CowLeg c=new CowLeg(1.2,"red");//若想訪問其示例成員,需要先創建一個內部類對象,包括訪問或修改private成員。c.length=2.3;c.color="blue";c.info();}public static void main(String[] args){Cow c=new Cow(350);c.test();} }2.12.2、靜態內部類
??????由 static 來修飾的內部類屬于外部類本身,而不屬于外部類的某個對象。稱其為靜態內部類,它是外部類的一個靜態成員。
??????1,靜態內部類可以包含靜態成員和非靜態成員。靜態內部類作為外部類的一個靜態成員,在靜態內部類中,不能訪問外部類的實例成員,只能訪問外部類的類成員。
public class Cow {private int p = 5;private static int p1 = 9;static class StaticClass {private static int age;public void accessP() {//System.out.println(p);//無法訪問。System.out.println(p1);}} }??????2,外部類可以通過類名來訪問靜態內部類的成員,對于靜態內部類的類成員,采用靜態內部類的類名來調用;對于靜態內部類的實例成員,采用靜態內部類對象來調用。
public class Cow {static class StaticClass {private static int age=18;private int num=10;}public void info(){//System.out.println(age); can't access//System.out.println(num); can't accessSystem.out.println(StaticClass.age);System.out.println(new StaticClass().num);}public static void main(String[] args){Cow c=new Cow();c.info();} }??????在接口中定義內部類或子接口,默認采用 public static 修飾該內部類或子接口。
2.12.3、使用內部類
??????如果內部類可以在外部類以外被訪問,那么內部類完整的類名為:OuterClass.InnerClass 。
??????1.1,在外部創建一個非靜態內部類對象如下,注意:非靜態內部類的構造器必須由其外部類對象來調用。
class Out {//此非靜態內部類為包訪問權限class In {public In(String msg){};} } public class CC{public static void main(String[] args){Out.In innerClassName=new Out().new In("haha");//和下面創建一個內部類對象等效。Out outer =new Out();Out.In in=outer.new In("hh");//非靜態內部類的構造器必須由其外部類對象來調用} }??????1.2,由非靜態內部類生成一個子類時,必須有一個外部類對象存在,不然無法調用內部類的構造函數,示例如下:
class Out {//此非靜態內部類為包訪問權限class In {public In(String msg){};} } public class CC extends Out.In{public CC(Out out){out.super("hello"); //這里的 super 指的是 In 的構造函數}public static void main(String[] args){CC c=new CC(new Out());} }??????2.1,在外部創建一個靜態內部類對象時,不需要額外創建外部類對象了。
class Out {//此靜態內部類為包訪問權限static class In {} } public class CC{public static void main(String[] args){Out.In in=new Out.In();} }??????2.2,由靜態內部類生成一個子類時,無需提供外部類對象。示例如下:
class Out {//此靜態內部類為包訪問權限static class In {public In(String a){};} } public class CC extends Out.In{public CC(String a){super(a);//不需要再傳入 Out 類對象了。}public static void main(String[] args){CC c=new CC("haha");} }2.12.4、局部內部類
??????局部內部類即定義在某個方法內的內部類,它僅在方法內有效(局部變量),因為局部內部類不能在方法以外使用,故其不能使用訪問控制符和 static 修飾符修飾。
??????局部內部類的定義變量、創建實例和派生子類都只能在局部內部類所在的方法中進行。如下:
public class CC{public static void main(String[] args){class InnerClass{// 局部類int a; //包訪問權限private double aa;//私有權限//暫未提供方法}class InnerSub extends InnerClass{int b;}InnerSub c=new InnerSub();c.a=0;c.b=1;} }??????但在實際開發中很少用到局部內部類,因為它的作用域太小了,只能在當前方法中使用。
2.12.5、匿名內部類
??????匿名內部類適合創建那種只需要使用一次的類,即創建匿名內部類時會立即創建一個該類的實例,這個類定義立即消失,匿名內部類無法重用。(因此匿名內部類不能是一個抽象類,即必須實現所有抽象方法,不然無法創建一個實例。當然若是有需要的話也可以重寫父類中的普通方法;也無法在匿名內部類中定義一個構造器,因為沒有類名,但初始化工作可以采用實例初始化塊來完成。)
??????如果想要對某個接口實現類重復使用,應該創建一個獨立類。
??????匿名內部類必須繼承但最多一個父類、或實現最多一個接口。格式為: new 實現接口() | 父類構造器(實參列表){ //匿名內部類的類體部分}
??????1,最常見的創建匿名內部類的方式是需要創建某個接口類型的對象。
interface Product{double getPrice();String getName(); } public class CC{public void test(Product p){System.out.println(p.getPrice() +" "+ p.getName());}public static void main(String[] args){CC c=new CC();c.test(new Product(){public double getPrice(){return 3.14;}public String getName(){return "pi";}});} }??????2,使用匿名內部類來繼承抽象父類:
abstract class Device{private String name;public abstract double getPrice();public Device(){};public Device(String name){this.name=name;}public String getName(){return name;} } public class CC{public void test(Device p){System.out.println(p.getPrice() +" "+ p.getName());}public static void main(String[] args){CC c=new CC();/* c.test(new Device("abcde"){public double getPrice(){return 314.15;}}); // 調用帶參數的構造函數。 */c.test(new Device() {@Overridepublic double getPrice() {return 314.15;}public String getName(){ //重寫父類的 getName 方法return "圓";}});} }??????3,局部內部類、匿名內部類訪問的局部變量必須使用 final 修飾。Java 8 之前只有 final 局部變量才可以被這二者訪問,Java 8 之后,如果一個被訪問的局部變量哪怕不是 final 類型,也會等價于 final 類型,不能對其重新賦值。見下例:
abstract class Device{public abstract double getPrice(); } public class CC{public void test(Device p){System.out.println(p.getPrice());}public static void main(String[] args){int a=9;CC c=new CC();c.test(new Device(){public double getPrice(){System.out.println(a);return 314.15;}}); //a=8; 會報錯。} }2.13、Lambda表達式與函數式接口使用簡介
??????Lambda表達式自 Java 8 開始增加,它允許使用更簡潔的代碼來創建只有一個抽象方法的接口的實例。只有一個抽象方法的接口也被稱為函數式接口,函數式接口也可以有自己的默認方法和類方法。 匿名內部類的使用范圍是 Lambda 表達式的超集(不局限于僅一個抽象方法),并且匿名內部類還可以應用于抽象類。
??????通過下面這個例子來比較一下匿名內部類和 Lambda 表達式的差異:
interface Process{void processInt(int element); } public class CC {void test(int[] a,Process b){for(int i:a){b.processInt(i);}}public static void main(String[] args){int[] array={1,2,3,4};CC c=new CC(); /* c.test(array, new Process() {@Overridepublic void processInt(int element) {System.out.print(element+1 +" ");}}); */c.test(array,(int element)->{System.out.print(element+1 +" ");});} }??????匿名內部類和 Lambda 表達式的實現效果一模一樣,但是 Lambda 表達式要更簡潔一些。它不需要寫 new Xxx() 、不需要指出重寫的方法名和返回值類型,而只需要給出重寫的用圓括號括起來的形參列表,、-> 和用大括號括起來的方法體即可。
??????1,如果形參列表中只有一個參數,那也可以不用圓括號括起來。
??????2,中間采用箭頭(->)連接。
??????3,方法體(代碼塊):如果方法體中只有一條語句,可以省略大括號;如果方法體中只有一條 return 語句,return 關鍵字也可以省略。
??????如下表:
| int add1(int a,int b); | (a,b)->{ int c=a+b; return c;}【通用格式】 |
| void taste(); | ()->System.out.println(“Good”) |
| void fly(String wea); | wea->{System.out.println(“Good”);System.out.println(“Good”)} |
| int add(int a,int b); | (a,b)->a+b |
2.14、枚舉類簡介
??????如果一個類的對象是有限且固定的,比如季節類,稱其為枚舉類。枚舉類采用關鍵字 enum 來定義。枚舉類是一種特殊的類,它一樣可以有自己的成員變量、方法,包括去實現接口,定義自己的構造器。一個 Java 源文件最多只能定義一個 public 訪問權限的枚舉類,且該源文件名也必須和該枚舉類的類名相同。(enum 和 calss、interface 地位相同)
??????使用 enum 定義、非抽象的枚舉類默認會使用 final 來修飾。(即不可變類,不能派生出子類)
??????枚舉類的所有實例必須在枚舉類的第一行列出,并且系統會自動添加 public static final 修飾。
??????枚舉類默認提供了一個values() 方法,可用來遍歷所有的枚舉值。示例:
public enum Season {//第一行列出 4 個枚舉實例SPRING,SUMMER,FALL,WINTER; //這四個枚舉值代表了本枚舉類所有可能的實例。 } public final class CC {public void judge(Season a){switch(a){case SPRING:System.out.println("春天");break;case SUMMER:System.out.println("夏天");break;case FALL:System.out.println("秋天");break;case WINTER:System.out.println("冬天");}}public static void main(String[] args){for(Season s:Season.values())System.out.println(s);new CC().judge(Season.SPRING);} }2.15、對象和垃圾回收
??????當程序創建對象、數組等引用類型實體時,系統都會在堆內存中為之分配一塊內存區,對象就保存在這塊內存區中,當這塊內存不再被任何引用變量引用時,這塊內存就變成了垃圾,等待垃圾回收機制進行回收。
??????1,垃圾回收機制值負責回收堆內存中的對象,不會回收任何物理資源(如數據庫連接、網絡I/O等)
??????2,程序無法精確控制垃圾回收的運行,垃圾回收會在合適的時候進行。當對象永久性地失去引用后,系統就會在合適的時候回收它所占的內存
??????3,在垃圾回收機制回收任何對象之前,總會先調用它的 finalize() 方法,該方法可能會使對象重新復活(讓另一個引用變量重新引用該對象),從而導致垃圾回收機制取消回收。
??????一、對象在內存中的狀態
??????可達狀態:當一個對象被創建后,若有一個以上的引用變量引用它,則這個對象在程序中處于可達狀態,程序可通過引用變量來調用對象的實例變量和方法。
??????可恢復狀態:如果程序中某個對象不再有任何引用變量引用它,它就進入了可恢復狀態。在這種狀態下,系統的垃圾回收機制準備回收該對象所占用的內存,在回收該對象之前,系統會調用所有可恢復狀態對象的 finalize() 方法進行資源清理。如果系統在調用 finalize() 方法時重新讓一個引用變量引用該對象,則這個對象會再次變為可達狀態;否則進入不可達狀態。
??????不可達狀態:當對象與所有引用變量的關聯都被切斷,且系統已經調用所有對象的 finalize() 方法后依然沒有使該對象變為可達狀態,那該對象永久地失去引用,最后變為不可達狀態。處于不可達狀態的對象,系統才會真正回收該對象所占用的資源。
public static void main(String[] args){String a=new String("haha"); // 1a=new String("yingyingying"); // 2}??????在 1 處,引用變量 a 指向 haha 對象,該行代碼結束后,haha 對象處于可達狀態。 當程序執行完代碼 2 之后,haha 對象處于可恢復狀態,yingyingying 對象處于可達狀態。
??????一個對象可以被局部變量引用,可以被其他類的類變量引用,也可以被其他對象的實例變量引用。被類變量引用的,只有該類被銷毀后,該對象才會進入可恢復狀態;被實例變量引用的,只有該對象被銷毀后,該對象才會進入可恢復狀態;
??????二、強制垃圾回收
??????系統何時調用 finalize() 方法對失去引用的對象進行資源清理,何時它會變為不可達狀態,何時回收它所占用的內存,對于程序完全透明。程序只能控制一個對象何時不再被任何引用變量引用,決不能控制它何時被回收。
??????這里的強制垃圾回收指的是通知系統進行垃圾回收,但系統是否進行垃圾回收依然不確定。但在大多數情況下,總會有一點效果的。強制系統垃圾回收的方式有兩種,任選其一即可:
??????調用 System 類的 gc() 靜態方法:System.gc()
??????調用 Runtime 對象的 gc() 實例方法:Runtime.getRuntime().gc()
??????下面的程序運行之后,沒有任何輸出,直到程序退出,系統都不曾調用 CC 對象的 finalize() 方法。
public final class CC {public void finalize(){System.out.println("清理資源中...");}public static void main(String[] args){for(int i=0;i<4;i++){new CC();}} }??????在 for 循環中添加代碼 System.gc(); 即可看到 4 次調用 CC 對象的 finalize() 方法來進行垃圾回收。
public final class CC {public void finalize(){System.out.println("清理資源中...");}public static void main(String[] args){for(int i=0;i<4;i++){new CC();System.gc();//Runtime.getRuntime().gc(); 效果一樣}} }??????三、finalize() 方法
??????在垃圾回收機制回收某個對象所占用的內存之前,通常要求程序調用適當的方法來清理資源,在沒有明確指定清理資源的情況下,Java 會采用默認機制 finalize() 方法來清理資源。它是 Object 類中的實例方法,原型為:protected void finalize() throws Throwable
??????在 finalize() 方法返回后,對象消失,垃圾回收機制才開始執行。
??????任何 Java 類都可以重寫 Object 類的 finalize() 方法,在該方法中清理該對象占用的資源。但如果程序終止之前始終沒有進行垃圾回收,則不會調用已失去引用的對象的 finalize() 方法來清理資源。而垃圾回收機制何時調用 finalize() 是透明的,只有當程序認為需要更多額外內存時,垃圾回收機制才會進行垃圾回收。因此當內存空間充裕,且失去引用的對象占用內存空間很小,則永遠也不會進行垃圾回收,不會調用 finalize() 方法。
??????一般地:1,永遠不要主動調用某個對象的 finalize() 方法,將其交給垃圾回收機制調用。2, finalize() 何時被調用、是否被調用具有不確定性。3,JVM 執行可恢復對象的 finalize() 方法時,有可能將該對象或系統中其他對象重新變為可達狀態。4,JVM 執行 finalize() 方法出現異常時,垃圾回收機制不會報告異常,程序繼續執行。
??????所以最好不要將清理某個類中資源的操作放在 finalize() 方法中,因為它不一定會執行。當然,有別的方法用于清理資源。
??????關于 finalize() 方法將可恢復狀態的對象變為可達狀態的示例如下:
public final class CC {private static CC c=null;public void info(){System.out.println("清理資源中...");}public void finalize(){c=this;//讓類變量 c 指向本程序中的可恢復對象 new CC();}public static void main(String[] args){new CC();//該對象創建完畢之后立即進入可恢復狀態System.gc();//通知系統進行資源回收 1System.runFinalization();//強制垃圾回收機制調用可恢復對象的 finalize() 方法。 2//Runtime.getRuntime().runFinalization(); 這個也可以c.info();} }??????該程序會有輸出。表示可恢復對象 new CC() 被類變量 c 所引用,從而可以調用 info() 方法,得到輸出:清理資源中…
??????如果取消 1 處的通知系統資源回收代碼后,系統通常不會立即進行垃圾回收(內存并沒有緊張),也就不會調用 new CC() 對象的 finalize() 方法,而在 c.info() 中,c 為 null ,從而引發空指針異常。
??????如果取消 2 處的強制垃圾回收機制調用可恢復對象的 finalize() 方法,系統僅執行 System.gc() 方法,由于 JVM 垃圾回收機制的不確定性,通常并不會立即調用可恢復對象的 finalize() 方法,所以在 c.info() 中,c 仍然可能為 null ,從而引發空指針異常。
總結
??????本文簡單介紹了一下 Java 面向對象的細節知識,由于自己有一些基礎,故有些地方就一筆帶過了。
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的Java 面向对象细节的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LeetCode算法题0:分发糖果【贪心
- 下一篇: 静态程序分析chapter1 - 概述和