對於像Java這樣的純物件導向語言,類是最基本的抽象單位,一直以來我總是希望自己能寫出一篇文章,來好好的梳理下自己對類和對象的理解。今天,這個願望似乎要實現了!不知從哪個地方寫起,因為這方面設計的東西太多了,說到類,就不難想到繼承、多態、封裝,就不難想到範圍及生命週期,很多的東西一下子湧上心頭,讓我不知道該從哪兒下手。本章系Java之美[從菜鳥到高手演變]系列之類與對象,希望通過我的分析,能讓讀者朋友們更加牢固記住相關的知識點,掌握類與對象方面的精髓!
閱讀過程中有任何問題,請聯絡egg:
郵箱:xtfggef@gmail.com 微博:http://weibo.com/xtfggef
如有轉載,請說明出處:http://blog.csdn.net/zhangerqing
一、類的建立及初始化
類通常是一類事物的抽象,如人就是一個類,你、我、他是這個類的具體執行個體,也就是對象。在Java中我們可以通過形如:class A {}來建立一個類,我們說過Java是物件導向的語言,每個對象都應該擁有它自己的屬性和方法,就拿人來說,膚色、身高等是人的屬性,吃、喝、玩等都是方法,也就是說屬性描繪了類的特點,而方法描述了類的功能,體現在Java的類中就像下面的代碼這樣:
public class Person {String name;int age;void eat(){}}
在物件導向的思想中,一切物體皆對象,我們以對象為單位進行編程,將這個對象所有的屬性方法封裝在一起,就是封裝。一般情況,我們通過類的構造器來建立類對象,構造器是一個擁有和類名同樣的名字的方法,我們可以對它傳遞參數,進行一些初始化工作,如,當我們需要在建立對象的時候,初始化其姓名及年齡,我們可以這樣來做:
public class Person {String name;int age;public Person(String name,int age){this.name = name;this.age = age;}void eat(){}}
測試類別中:
public class ClassTest {public static void main(String[] args) {Person person = new Person("egg",23);}}
new 操作符會為我們在記憶體中開闢空間,person是對象名,也是引用,在棧上分配,指向由new在堆上分配的內容,具體的JVM記憶體管理,請看我的另一篇博文:JVM記憶體管理與記憶體回收,裡面有詳細的介紹,此處非重點,不去深講。我們再來分析一下這個過程:當調用new Person()時,編譯器首先檢查下原類Person中是否有Person()構造方法,此處因為有public Person(String name,int age),所以new的時候,直接調用的該方法,但是很多時候,我們並沒有顯示聲明構造方法,此時,編譯器在調用的new Person()的時候,會自動為我們的Person類添加一個無參的空Person()構造方法:Person(){},來執行類的構造過程。說到構造方法,我們來看看下面的這段代碼:
public class Person {public Person(int id) {System.out.println("person(" + id + ")");}public static void main(String[] args) {Build b = new Build();}}class Build {Person p1 = new Person(1);public Build() {System.out.println("this is build's block!");Person p2 = new Person(2);}Person p3 = new Person(3);}
此處我主要想說明,用構造器建立類和變數的初始化順序,該程式輸出:
person(1)
person(3)
this is build's block!
person(2)
說明:不論變數放在哪兒,都會先於任意一個方法的執行前執行,包括構造方法,而構造方法是一個類必須會執行的方法,不需要顯示的進行調用。同時,不論變數在哪兒分布,只要在方法外部,就一定先於方法初始化。說到這兒,我們不得不談一下另一個關鍵的知識點靜態塊和非靜態塊。二者都有很簡單的聲明方式,只需一對大括弧{}就行,靜態塊的話,在前面加static關鍵字,我們寫個小程式來看看:
public class Person {/*靜態塊*/static{System.out.println("this is static block!");}/*非靜態塊*/{System.out.println("this is non-static block!");}public Person(int id) {System.out.println("person(" + id + ")");}public static void main(String[] args) {Person p1 = new Person(1);Person p2 = new Person(2);}}
該程式輸出:
this is static block!
this is non-static block!
person(1)
this is non-static block!
person(2)
說明什麼問題?觀察一下:我們new了兩個對象,可是靜態塊只執行了一次,而非靜態塊執行了兩個,且都是在調用構造器之前。我們似乎得出了一些結論:靜態塊是在類的裝載時執行的(裝入.class檔案後),且只執行一次。而非靜態塊是在調用構造方法之前執行的,每產生一個執行個體對象,就會調用一次非靜態塊。此處,我想引入一個很重要的知識點:static關鍵字。一般來說,被聲明為static的變數或者方法,或者前面說的塊,都屬於類變數、類方法,屬於類的屬性資訊(在方法區分配記憶體)。如靜態塊一樣,其它的待用資料也具有這個特點:初始化只在類裝載的時候執行一次。對於類變數和類方法,還有一個重要的特點就是,外部對象對他們的引用可以直接通過類名來調用,如下面的代碼:
public class Person {static int id = 10;}class Test{public static void main(String[] args) {System.out.println(Person.id);}}
除了使用new操作符,我們還有一些其它方法來建立類,如Java的反射機制,我們會有專門的博文來介紹相關的知識點。
我們總結下對象的建立過程:(含順序)根據下面這個程式的輸出:
public class Person {public Person(int id) {System.out.println("person(" + id + ")");}}class Build {/*靜態塊*/static{System.out.println("this is static block!");}/*非靜態塊*/{System.out.println("this is non-static block!");}Person p1 = new Person(1);//------------1-----------public Build() {System.out.println("this is build's block!");Person p2 = new Person(2);}Person p3 = new Person(3);public static void main(String[] args) {Build b = new Build();}}
this is static block!
this is non-static block!
person(1)
person(3)
this is build's block!
person(2)
1、先裝載.class檔案,建立Class對象,對待用資料(由static聲明的)進行初始化,而且只進行一次初始化。
2、new Build()在堆上進行空間分配。
3、執行非靜態塊。
4、執行所有方法外定義的變數的初始化。
5、執行構造器。
我們再來看個例子:將static放在上述例子的注釋1行之前,輸出會有變化:
this is static block!
person(1)
this is non-static block!
person(3)
this is build's block!
person(2)
正好驗證了我們上面的結論,總體來說執行順序為:靜態塊,靜態屬性->非靜態塊,屬性->構造器。接下來我們分析一下類的屬性和方法。
補充:此處經網友a52360509指正,對於靜態塊和靜態屬性或者非靜態塊和屬性,初始化順序決定於它們在代碼中的順序。
屬性:
類中的屬性一般分為類屬性(全域變數)、執行個體屬性(全域變數)、局部屬性(局部變數)。<我是這麼分的,儘管有人不這麼分,但是分法無所謂,理解它們的含義最重要>.
類屬性:前面已經說過就是那些聲明為static的屬性,在整個過程中只進行一次初始化,在記憶體中只開闢一個空間,不論在哪兒調用,值保持一致。一旦被修改,所有引用它的地方都會跟著修改。一般直接通過類名進行調用。
執行個體屬性:執行個體變數是可以不進行初始化,比如一個整型的執行個體變數假如沒有初始化,則預設值為0;而局部變數假如不賦初值文法上是通過的,但是在使用這個變數是程式就報錯了。執行個體變數在堆和棧中都分配記憶體空間,在堆當中分配的是對象本身,而棧中則是對這個對象的引用。
局部屬性:局部變數是在方法內部聲明的變數,生命期僅在方法內,方法結束後變數就消失了;局部變數必須初始化再使用,否則會報錯,也就是說,假如你在方法內定義了一個局部變數,並且沒有賦值,那麼你在使用這個變數的時候一定得賦值,不然就報錯了。同時,局部變數可屏蔽全域變數。
方法:
方法就是類的行為,形如:
public void say(){dosomething;...}
由方法頭和方法體構成,方法頭包括:存取控制符(如public private等)、傳回型別(傳回型別決定該方法在調用後產生的結果的類型)、方法名、參數列表組成。聲明為void的方法,傳回值為空白。在特殊的情況下,我們可以為方法添加一些特殊的關鍵字以實現特殊的功能,如synchronized、final、static、abstract等等。方法方面我只想介紹兩個重要的方面:重載(Overload)和重寫(Override亦叫覆寫)。
重載:
是指在同一個類中,具有相同的方法名,不同的參數列表的方法之間的一種機制。參數列表的不同體現在:類型不同、個數不同、順序不同,只要滿足任一一個,就可以進行方法重載。
public class AreaCal {/* 計算長方形的面積 */public int area(int width, int height) {return width * height;}/* 計算正方形的面積 */public int area(int edge) {return edge * edge;}/* 計算圓的面積 */public double area(float radius) {return 3.14 * radius * radius;}}
如例所示,同一個方法名area,同步傳入不同的參數,實現不同的功能,這就是方法重載,用這樣的機制有什麼好處?個人感覺就是在模型上的一種統一,同樣的功能,調用同樣的方法,只需傳入不同的參數,增強了程式的可讀性和易於維護,當有很多個功能相似的方法的時候,如果我們為每個方法設計一個名稱,想通過名稱來區分它們的話,會很糟糕,而且會讓人覺得程式的可讀性差,設計不夠巧妙!此處問題來了:我們可不可通過方法的傳回值來區別方法重載?讓我們看個例子:
public int area(int width, int height) {return width * height;}public float area(int width, int height){return width * height;}
當其他環境調用這兩個方法時,並不會先得到他們的傳回值,傳回值只有在方法執行完畢才返回,在調用之初,編譯器無法判斷他們的區別,所以只能當同名方法來處理:報錯!所以試圖通過傳回值來進行方法重載是不正確的!
重寫:
重寫是在繼承中存在的,在兩個類(子類和父類之間存在的關係)中,子類重寫父類的方法,方法名相同,參數也相同的一種機制。
public class B extends A {public String a(String name) {return "welcome to you :" + name;}}class A {public String a(String name){return "hello:"+name;}}
當子類繼承了父類後,想對父類的方法功能進行擴充,就可以使用重寫,這樣做的目的就是:當你需要擴充新的功能的時候,不需要建立方法,在父類的方法中進行補充,這樣一種物件導向思維的體現,不用重寫同樣可以達到這樣的效果,但是用了重寫更加符合OO思想。類似的還有一種概念,叫隱藏:當子類和父類方法名相同,參數不同時,子類隱藏父類的方法實現,這就是一種機制,一種叫法,沒什麼實際意義,相當於我們建立了方法,只是方法名和父類的相同,但是不是父類的方法的實現。
Java中不定參數調用
有的時候,我們不能確定方法的參數個數,我們可以採取這種機制(String ... value),如下面的代碼:
public class B{public static String a(String ... value) {return "welcome to you :" + value;}public static void main(String[] args) {System.out.println(a("egg"));}}
列印出來的結果是:welcome to you :[Ljava.lang.String;@61de33。後面這部分資訊一看就是數組的資訊,所以我們繼續研究,發現:當你將參數寫出String ... value的形式的時候,已經預設相當於:String[] value 因此你需要在方法內部擷取的資料的時候寫成數組下標的形式:
public static String a(String ... value) {return "welcome to you :" + value[0];}
當有多個參數時,同樣用下標來取值:
public class B {public static String a(String... value) {return "welcome to you :" + value[0] + value[1] + value[2];}public static void main(String[] args) {System.out.println(a("egg", ":niu", ":baby!"));}}
這種機制在有些開發中特別有用,降低代碼量,使程式更加簡潔,不過有時會犧牲一點可讀性。
二、類與對象的關係
經過上面這麼多的演練,我們總結下類和對象的關係:看看下面的代碼:
public class B {public static void main(String[] args) {B b = new B();A a = new A();}}class A {}
這段代碼中,A和B都是類,a和b屬於對象,他們之間是種什麼關係呢?
1、類是一類具有相同屬性的事物的統稱,是一種抽象。
2、對象是類的具體體現,又稱執行個體。
3、類是一種靜態概念,而對象是一種動態機制。
每個人都有不同的看法,還有什麼關係,希望大家補充,筆者真心希望各位讀者能認真思考,積極提出寶貴的建議!因為有不少讀者提出之前的博文篇幅過長,希望我盡量寫的短一點兒,方便閱讀,所以我將其它內容放在了下一篇,希望朋友們能夠繼續閱讀!
本章完。