零基礎學Java第五節(面向物件一)
本篇文章是《零基礎學Java》專欄的第五篇文章,文章採用通俗易懂的文字、圖示及程式碼實戰,從零基礎開始帶大家走上高薪之路!
本文章首發於公眾號【程式設計攻略】
類與物件
在哲學體系中,可以分為主體(subject)和客體(object),在面向物件的程式設計語言中,所有的要面對的事物都可以抽象為物件(object)。在面向物件的程式設計過程中,我們就是使用各種各樣的物件相互協同動作來完成我們的程式功能。
在面向物件的語言中,所有使用中的物件都具有某種型別,這些型別之間也有層次關係,如同生物學中的門、綱、目、科、屬、種一樣,這種層次關係,我們可以用繼承這個機制來完成。
Java語言為面向物件的語言,所有的物件都可以具有某些屬性,以及具有某種行為功能。在Java語言中物件的屬性用成員變數(域)描述,行為用方法描述。
類和物件之間的區別和聯絡就是,類是抽象的,它具有一類物件所有的屬性和行為,相當於模板,物件是具體的,通過建立相應類的物件來完成相應的功能。我們在作面向物件的程式設計的時候,就是抽象出類的屬性和行為,再建立具體的物件來完成功能。
Date類
定義
在JDK中,有一個用於描述日期的類:java.util.Date
,它表示特定的瞬間,精確到毫秒。在這裡我們自己定義一個簡單的Date類,用來表示具有年、月、日的日期。我們先來看看如下定義怎麼樣?
public class Date{
public int year;
public int month;
public int day;
}
在這段程式定義中,類定義為public,同時year、month、day也定義為public,它們是什麼含義呢?
我們前面提到包這個概念,我們說,在同一個包中的類在功能上是相關的,但是在包內的這些類並非全部都可以被其他包中的類呼叫的,怎麼區分呢?那些可以被包外使用的類,我們在定義的時候,在類名前加上public,而不能被包外使用的類,在定義的時候,不要加public。
year、month、day這些在類體中所定義的變數,我們稱之為成員變數(域 field),那麼成員變數前面的public的作用是說明這些變數是公開的,可以被以 物件.變數
這種形式呼叫,這稱為成員變數的訪問許可權。比如本例:我們定義一個Date
型別的變數d1
,那麼我們可以通過d1.year
來使用d1
這個物件中的year
。在定義成員變數的時候,除了可以定義為public
protected、預設(無許可權修飾詞)、private
,它們的限制越來越嚴格,我們總結一下:
- public:被public修飾的成員是完全公開的,可以被以任何合法的訪問形式而訪問
- protected:被protected修飾的成員可以在定義它們的類中訪問,也可被同一包中的其他類及其子類(該子類可以同其不在同一個包中)訪問,但不能被其他包中的非子類訪問
- 預設:指不使用許可權修飾符。該類成員可以在定義它們的類中被訪問,也可被其同一包中的其他類訪問,但不能被其他包中的類訪問,其子類如果與其不在同一個包,也是不能訪問的。
- private:只能在定義它們的類中訪問,僅此而已。
在類中,我們不但可以定義成員變數,還可以定義成員方法,成員方法前面也可以有這四種許可權控制,含義是一樣的。但是大家千萬不要以為在獨立類的前面也可以有private、protected這兩個修飾詞,在類前只能有public或者沒有,含義前面已述。
關於public類和源程式檔案的關係,我們在以前學習過,這裡再重新提一下:
每個原始碼檔案中至少定義一個類,若有多個類,最多只有一個類定義為public,若有public類,則該原始碼檔案的字首名字要同該類的類名完全一致,若沒有
public
類,則原始碼檔案的字首名可不與檔案內的任何類一致。
測試
上面的Date
類已經定義好了,我們怎麼測試這個類呢?我們在這個類中定義main
方法了嗎?沒有main
方法的類,可以以 java 類名
這種方式在控制檯中執行嗎?顯然不行,那怎麼辦呢?有兩種方案,一種是我們給它定義一個main
方法;另一種方案,再定義一個類,專門測試這個Date
類。我們依次來看看。
新增main方法
我們在Date
的定義中加入main
方法,程式碼如下:
public class Date{
public int year;
public int month;
public int day;
public static void main(String[] args){
year = 2016;
month = 9;
day = 5;
System.out.println("year:" + year + " month:" + month + " day:" + day);
}
}
編譯...,怎麼?又錯了?
別慌,看看出錯提示,都是無法從靜態上下文中引用非靜態 變數
這樣的提示,這是什麼意思?我們看看main方法前面是不是有個修飾詞static,我們前面提到凡static修飾的成員變數或者方法,都可以用 類名.成員
這種形式來訪問。如果Date類在控制檯中以 java Date
這種命令形式來執行,我想請問,系統是建立了一個Date物件了,然後再執行這個物件中的main方法這種方式來執行的Date類嗎?錯,系統是直接找到Date.class這個類檔案,把這個類調入記憶體,然後直接執行了main,這時在記憶體中並沒有Date物件存在,這就是為什麼main要定義為static,因為main是通過類來執行的,不是通過物件來執行的。
那上面的解釋又同這個編譯錯誤提示什麼關係?我們看看 year、month、day前面有沒有static這個修飾詞?沒有,這說明這些成員變數不能通過類名直接訪問,必須通過建立類的物件,再通過物件來訪問。而main在執行的時候,記憶體中並沒有物件存在,那自然那些沒有static修飾的成員就不能被訪問,因為它們不存在,是不是這個邏輯?想明白了嗎?還沒有?再想想!
我們得出個能記住的結論,在static修飾的方法中,只能使用本類中那些static修飾的成員(包括成員變數、方法等),如果使用本類中非static的成員,也必須建立本類的物件,通過物件使用。好吧,不明白道理,就先把這個結論記著。
我們根據上面的結論,把測試程式碼再改改:
public class Date2{
public int year;
public int month;
public int day;
public static void main(String[] args){
Date2 d1 = new Date2();
d1.year = 2016;
d1.month = 9;
d1.day = 5;
System.out.println("year:" + d1.year + " month:" + d1.month + " day:" + d1.day);
}
}
編譯...,OK,執行,通過,
那如果非靜態方法中使用靜態成員,可不可以呢?我們說可以,簡單講,這是因為類
先於物件
存在,靜態成員依賴於類
,非靜態成員依賴於物件
。
上面的程式中,我們給這幾個成員變數分別給了值,如果不給值直接輸出呢?我們試試:
public class Date3{
public int year;
public int month;
public int day;
public static void main(String[] args){
Date3 d1 = new Date3();
System.out.println("year:" + d1.year + " month:" + d1.month + " day:" + d1.day);
}
}
編譯,執行,結果: ,都是0。這裡有個結論:成員變數如果沒有顯式初始化,其初始值為0值,或者相當於0值的值
,比如引用變數,未顯式初始化,其值為null,邏輯變數的未顯式初始化的值為false。這裡要注意,我們說的是成員變數,如果是區域性變數,沒有顯式初始化就用,編譯是通不過的。比如上面的程式碼,改為:
public class Date3{
public int year;
public int month;
public int day;
public static void main(String[] args){
Date3 d1;//這裡d1為區域性變數,沒有初始化
System.out.println("year:" + d1.year + " month:" + d1.month + " day:" + d1.day);
}
}
編譯: 從編譯提示,我們知道d1未初始化就用,這是不對的。
建立一個測試類
程式碼如下:
//本類放在Date.java檔案中
public class Date{
public int year;
public int month;
public int day;
}
//本類放在TestDate.java檔案中
public class TestDate{
public static void main(String[] args){
Date d1 = new Date();
d1.year = 2016;
d1.month = 9;
d1.day = 5;
System.out.println("year:" + d1.year + " month:" + d1.month + " day:" + d1.day);
}
}
這兩個類都是public類,所以,這兩個類應該寫在兩個不同的原始碼檔案中,檔名分別為:Date.java
與TestDate.java
中。如果大家想將這兩個類寫在一個原始碼檔案中,因為Date類在測試完成以後是會被公開使用的,所以我們應該把Date定義為public,那麼原始碼檔案的名字就是Date.java
,而TestDate這個類是用於測試Date類的,所以它前面的public就應該去掉。程式碼如下:
//這些程式碼放在Date.java檔案中
class TestDate{
public static void main(String[] args){
Date d1 = new Date();
d1.year = 2016;
d1.month = 9;
d1.day = 5;
System.out.println("year:" + d1.year + " month:" + d1.month + " day:" + d1.day);
}
}
public class Date{
public int year;
public int month;
public int day;
}
這兩個類的定義順序無關緊要。編譯後,我們在命令列上執行:java TestDate
即可。
思考
封裝
在這個示例中,有個問題:我們在建立Date
物件d1
以後,就直接使用d1.
這種形式給year、month、day賦值了,如果我們給它們的值不是一個合法的值怎麼辦?比如month的值不在1~12
之間,等等問題。在如上的Date定義中,我們沒有辦法控制其他程式碼對這些public修飾的變數隨意設定值。
我們可以把這些成員變數用private修飾詞保護起來。由private修飾的成員,只能在類或物件內部自我訪問,在外部不能訪問,把上面的Date程式碼改為:
public class Date{
private int year;
private int month;
private int day;
}
這樣我們就不可以通過d1.year
來訪問d1物件中year
了。慢著,不能訪問year,那這個變數還有什麼用?是的,如果沒有其它手段,上面的這個類就是無用的一個類,我們根本沒有辦法在其中存放資料。
怎麼辦?我們通過為每個private的成員變數增加一對public的方法來對這些變數進行設定值和獲取值。這些方法的命名方式為:setXxx和getXxx,setter方法為變數賦值,getter取變數的值,布林型別的變數的取值用:isXxx命名,Xxx為變數的名字,上例改為:
public class Date{
private int year;
private int month;
private int day;
public void setYear(int year){
//理論上year是沒有公元0年的,我們對傳入的值為0的實參處理為1
//year的正值表示AD,負值表示BC
if(year == 0){
//這裡有兩個year,一個是成員變數,一個是實參,這是允許的
//為了區分它們,在成員變數year前面加上this.
//實參year不做處理
//如果沒有變數和成員變數同名,this是可以不寫的
this.year = 1;
} else {
this.year = year;
}
}
//因為要取的是year的值,所以getter方法的返回值同所取變數的型別一致
public int getYear(){
return year;
}
public void setMonth(int month){
if((month > 0) && (month < 13)){
this.month = month;
} else{
this.month = 1;
}
}
public int getMonth(){
return month;
}
public void setDay(int day){
//這個方法有些複雜,因為我們需要根據year、month的值來判斷實參的值是否合規
switch(month){
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:if (day < 32 && day > 0) {//在1~31範圍內
this.day = day;
}else{
this.day = 1;//超出日期正常範圍,我們設為1
}
break;
case 4:
case 6:
case 9:
case 11:if (day < 31 && day > 0) {//在1~30範圍內
this.day = day;
}else{
this.day = 1;//超出日期正常範圍,我們設為1
}
break;
case 2:if (isLeapYear()) {
if (day < 30 && day > 0) {//在1~29範圍內
this.day = day;
}else{
this.day = 1;//超出日期正常範圍,我們設為1
}
} else {
if (day < 29 && day > 0) {//在1~28範圍內
this.day = day;
}else{
this.day = 1;//超出日期正常範圍,我們設為1
}
}
break;
default:this.day = 1;//如果month的值不在上述情況下,day設定為1
break;
}
}
//這個方法判斷年份是否為閏年,是閏年返回true,否則返回false
//該方法只在本類內部使用,所以定義為private
private boolean isLeapYear(){
//可被400整除或者被4整除但不能被100整除的年份為閏年,其它年份為平年
if((year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0))){
return true;
}
return false;//能執行這裡,說明是平年
}
}
經過上面的改造,雖然程式碼長了很多,但是安全了很多。我們對year、month、day的值不再是直接存取,而是通過相應變數的getter和setter方法來存取,這些方法在進行存取的時候,會判斷設定的值是否合規。
上面的程式碼其實是可以優化的,比如setDay可以優化如下:
public void setDay(int day){
//這個方法有些複雜,因為我們需要根據year、month的值來判斷實參的值是否合規
this.day = 1;//這裡先把day設定為1,下面的超範圍的情況就不用再寫程式碼了
switch(month){
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:if (day < 32 && day > 0) {//在1~31範圍內
this.day = day;
}
break;
case 4:
case 6:
case 9:
case 11:if (day < 31 && day > 0) {//在1~30範圍內
this.day = day;
}
break;
case 2:if (isLeapYear()) {
if (day < 30 && day > 0) {//在1~29範圍內
this.day = day;
}
} else {
if (day < 29 && day > 0) {//在1~28範圍內
this.day = day;
}
}
break;
}
}
我們通過上面的示例,看到了面向物件的一個概念:封裝。我們將資料用private隱藏起來,通過public存取方法對資料進行了封裝,使得資料更加安全與可靠。
成員變數的初始化
非靜態成員變數的初始化
在上面的例子中,如果我們新建一個Date物件以後,如果不初始化,我們可以知道,直接取其中的year、month、day的值的時候,它們的值都是0,這顯然是不合理的。要解決這個問題,我們需要在建立Date物件的時候,讓系統自動初始化一個合適的值,要達到此目的,我們可以採用三種方式:
-
在定義成員變數的時候初始化值,如上面的程式碼,我們修改一下如下:
public class Date{ private int year = 1; private int month = 1; private int day = 1; ... }
上面的程式碼,使得新建Date物件的初始值均為1。
-
第二種方式為使用構造方法
構造方法是類中一種特別的方法,它的方法名和類名一致,該方法沒有返回值,甚至連void都沒有,而且構造方法不能被當作普通方法一樣被呼叫,每當生成某個類的物件的時候,構造方法才被呼叫。構造方法的作用就是在建立物件的時候執行對物件的初始化工作。我們使用構造方法對成員變數來進行初始化,程式碼如下:
public class Date{ private int year; private int month; private int day; public Date(){ year = 1; month = 1; day = 1; } ... }
上面的程式碼使得我們在新建Date物件時,可以初始化其內的值。上面的構造方法沒有引數,這種構造方法,我們稱之為
預設構造方法
。那是不是用構造方法只能初始化為這種固定的值呢?能不能在建立物件的時候再指定初始化值呢?答案是可以的,就是使用有引數的構造方法。程式碼如下:public class Date{ private int year; private int month; private int day; public Date(int year, int month, int day){ //下面的程式碼使用setter方法進行初始化是因為,setter方法提供了引數檢查, //如果不用setter方法,我們就需要重新寫對引數的檢查程式碼 setYear(year); setMonth(month); setDay(day); } ... }
上面的程式碼使得我們可以在建立新的Date物件的時候指定初始化值,比如:
Date d = new Date(2016,9,5);
。但是上面的程式碼沒有定義無參構造方法,我們再用無參構造方法來建立Date物件就不行了,如:Date d = new Date();
,編譯時會出現如圖的錯誤:
這是為什麼呢?這是因為,如果定義類時沒有定義構造方法,則編譯器會自動建立一個無參的空的public構造方法,作為該類的構造方法。但是,只要你定義了構造方法,不管是有參還是無參,編譯器就不再自動生成了,就會出現上面的錯誤提示。另外,在構造方法中產生的異常會被jvm忽略,即使是在構造方法中使用try也無濟於事。
public class Test { public Test() { try { System.out.println("trying to throw an exception"); double x = 1.0/0.0;//此句會產生除0異常 } catch(Exception e) { System.out.println("Exception captured"); } finally { System.out.println("inside finally"); } } public static void main(String args[]) { Test test = new Test(); } }
上述程式碼在執行以後的結果如圖: 從圖大家可以看到發生的異常並未被catch。
-
第三種方式是使用例項語句塊。那麼什麼是例項語句塊呢?例項語句塊是由一對大括號括起來的可以被執行的語句序列,這個語句塊在類的內部,但不在任何方法的內部,如下程式碼:
public class Date{ private int year; private int month; private int day; //下面的大括號為例項語句塊 { year = 1; month = 1; day = 1; } ... }
上面的例項語句塊會在構造方法執行前被執行。如果出現多個例項語句塊,按順序執行。
-
如果這三種方式同時採用呢?程式碼如下:
public class Date{ private int year = 1; private int month = 1; private int day = 1; { year = 2; month = 2; day = 2; } public Date(){ year = 3; month = 3; day = 3; } ... }
上面的Date類,如果要新建物件,其初始值是1、2,還是3呢?在建立物件的時候,會在堆中為物件分配儲存空間再將所分配空間的所有內容都設為預設初始值0,接著將成員變數定義時的值對成員變數進行初始化,再執行例項語句塊,最後執行構造方法。因此,上例的初始最終值為3 。
靜態成員變數的初始化
-
我們前面所涉及到的是對非靜態成員變數的初始化,我們知道有一類成員變數是用static修飾的,這類成員變數是不依賴物件的,那麼這類變數如何初始化呢?
首先我們要明確,靜態成員變數的儲存空間不同任何該類的物件相關聯,它們的儲存空間是獨立的。由此,我們可以得出兩個結論,首先靜態成員變數的值只能被初始化一次,其次,靜態成員變數的值不能在構造方法中初始化(因為構造方法是在建立物件的時候呼叫的)。
那麼怎麼對靜態成員變數進行初始化呢?有兩種方式:- 象初始化非靜態成員變數一樣,直接給靜態成員變數賦初始值,如:
public class Test{ public static int first = 1; public static int second = 2; }
- 第二種,使用靜態語句塊,如下:
public class Test{ public static int first; public static int second; //下面的語句就是靜態語句塊,這種語句塊不能放在方法體中 static{ first = 1; second = 2; } }
在類中,可以出現多個靜態語句塊,多個靜態語句塊順序執行。
- 靜態成員變數的初始化發生在該成員變數第一次被使用的時候,之後該成員變數不會再重複初始化。而且類中的靜態成員變數的初始化發生在非靜態成員變數的初始化之前,這樣,下面的程式碼就不合適了:
public class Test{ public int first = 1; public static int second = first; }
- 象初始化非靜態成員變數一樣,直接給靜態成員變數賦初始值,如:
類定義中的成員
Java的類中可以包含兩種成員:例項成員和類成員。
例項成員
- 例項成員(包括例項成員變數及例項方法)屬於物件,通過引用訪問:
- 引用變數.例項成員名;
- 定義類時,如果成員未被static修飾,則所定義的成員為例項成員,如:
int i=10; void f(){…}
- 例項成員的儲存分配
- 通常,類只是描述,通過使用new,對物件進行儲存空間分配。未被static所修飾的成員是物件的一部分,因此,這種例項成員的儲存分配伴隨物件的分配而分配。
- 例如:
則此時,i有兩個副本,一個在t1中,一個t2中,通過t1.i及t2.i使用class T{ int i; void f(){} } 。。。 T t1,t2; t1=new T(); t2=new T();
類成員
- 類成員(包括類成員變數及類方法)屬於類,通過類名訪問,也可通過引用訪問:
- 類名.類成員名;
- 定義類時,如果成員被static修飾,則所定義的成員為類成員,如:
static int count=0; public static void main(String args[]){…}
- 類成員的儲存分配
- static用於成員變數前,則不管有多少物件,該成員變數只保留一份公共儲存,稱為類變數(靜態變數);
- 如果在定義方法時,使用static進行修飾,稱為類方法(靜態方法),則在呼叫該static方法,則該方法不依賴具體物件,既是你可以呼叫某個方法,而不用建立物件。
- 對於類成員的使用,即可以使用類名使用,也可以通過引用使用,如:
則此時,i只有一份,既通過t1.i及t2.i使用,也可通過T.i引用,其值均為47。在執行T.i++;後t1.i及t2.i的值均為48;對f的呼叫既可以以如t1.f();也可以T.f();方式使用。根據java程式設計規範,我們建議對於static成員,只採用類名引用的方式。class T{ static int i=47; static void f(){i++;} } 。。。 T t1,t2; t1=new T(); t2=new T();
類成員、例項成員總結
- 當你使用static就意味著,這個成員變數或方法不依賴於任何該類的物件,所以無需建立物件,就可使用該static資料或方法。
- 對於non-static 資料和方法則必須建立物件,然後使用該物件操作 non-static 資料和方法。
- 由此,因為 static 方法不需要建立物件,所以static 方法不能直接存取非static成員。
- 而non-static 方法則可以直接呼叫static成員
public class Test{
static int i=0;
int j=10;
static void f(){i++;}
void s(){f(); j++;}
public static void main(String[] args){
Test t=new Test();
t.s();
System.out.println(i);
}
}
- static的一個重要應用就是無需建立物件而呼叫方法。比如main( )
- 在定義一個類時,個性的定義為non-static,共性的定義為static
this
在Java中我們經常看到對this這個關鍵字地使用。它的使用有兩種場合:
-
this作為一個引用用來引用自身,每個物件都有一個this。在類的成員方法中,this用於表示對本物件內的方法或者變數的使用。如果在不會引起歧義的情況下,this是可以省略的。比如上面的一系列setter方法中,因為引數同成員變數同名,為了以示區別,成員變數前的this就不能省略了。
this不能用在靜態方法中。因為靜態方法的執行根本不需要java物件的存在,而是直接使用
類名.方法
的方式訪問,而this代表的是當前物件,所以在靜態方法中根本無法使用this。 -
this可以用在構造方法中,對本類中其它構造方法進行呼叫
- 語法:this([實參]);
- 作用:在一個構造方法中去呼叫另一個構造方法。
- 目的:程式碼重用。
- this(實參):必須是構造方法中第一個被執行的語句,且只被呼叫一次。
我們讀讀下面的程式碼
public class Flower { int petalCount = 0; String s = new String("null"); Flower(int petals) { petalCount = petals; System.out.println("Constructor with int arg only, petalCount= "+ petalCount); } Flower(String ss) { System.out.println("Constructor with String arg only, s=" + ss); s = ss; } Flower(String s, int petals) { this(petals); //呼叫Flower(petals),但不能寫作Flower(petals) //! this(s); // Can't call two! this.s = s; // 這是this的另一種使用方式 System.out.println("String & int args"); } Flower() { this("hi", 47); System.out.println("default constructor (no args)"); } void print() { //! this(11); // Not inside non-constructor! System.out.println("petalCount = " + petalCount + " s = "+ s); } public static void main(String[] args) { Flower x = new Flower(); x.print(); } } ///:~
private構造方法
在構造方法前也可以有public等修飾詞用於限定構造方法的存取許可權。一般情況下,如果類是public,構造方法也是public,這樣通過new
才能呼叫。所以,如果構造方法前面被private 修飾,那會發生什麼事情呢?我們在類外就不能生成該類的物件了。大家可以自己測試一下。
那是不是private就不能用在構造方法前面了呢?當然可以,而且這是一種很有用的情況。在有些情況下,有些類在系統中只能允許存在一個該類的例項(物件),這時,我們就可以把該類的構造方法定義為private的。示例程式碼如下:
public class Handler {
//handler變數用來儲存該類物件的引用
private static Handler handler = null;
private Handler() {
/* set something here */
}
//該類的物件只能通過getHandler方法從handler這個私有變數中獲取
public static getHandler(/* arglist */) {
//如果handler值非空,說明已經在系統中存在該類的物件,直接取出,不再生成,這就保證了單例
if (!handler)
handler = new Handler();
return handler;
}
public static void main(String args[]) {
Handler.getHandler();
}
}
最後
本文章來自公眾號【程式設計攻略】,更多Java學習資料見【程式設計攻略】