類與對象
java是一種面向對象的開發語言。java程序是由類與對象組成的。類與對象之間有什麼關係呢?
類是構造對象的藍圖或模板。由類構造對象的過程,稱之爲創建類的實例。可知對象就是類的一種實例或具體實現。爲什麼爲選用java語言做開發,這種面向對象的語言對開發有什麼好處?
首先,從設計上,對一個問題,你可以暫且不管它的具體實現是什麼,先把它抽象成一個對象,問題中涉及到的數據,變成對象中的實例域,求解問題的方法,變成對象中的方法,這樣做可以分清要對哪些數據進行操作,邏輯上比較清晰。
其次,當問題的求解較爲複雜時,可能有很多子方法,這時,我們可以按照模塊化的方式,將子方法抽取出來,依次實現,這樣可以很快求解出來,且不易出錯,即使出錯,也知道它錯在哪個地方(某個方法錯了,一目瞭然,相比較幾百行代碼而言)。
最後,如果求解問題時,發現與曾經求解的問題相似,是不是可以把之前求解的方法拿過來,然後在此基礎上修改,方便許多。
在上面求解問題的過程中涉及到的一些技術:封裝,繼承
封裝:類中的方法不能直接操作其他類的數據,而只能通過其他類實例化的對象調用方法,從而操作數據。
繼承:一個類在另一個類的基礎上擴展,擴展後的類具有被擴展類的方法和屬性。
對象與對象變量
要想使用對象,首先得構造對象,java中通過構造器構造並初始化對象。構造器的名字與類名相同,例如Date類,想要構造一個Date對象
new Date();
這個表達式構造了一個表示當前日期的對象。當然,可以把對象保存在一個對象變量中。
Date date=new Date();
這裏我要解釋一下對象與對象變量的區別
Date date2;//date doesn't refer to any object
//此時date僅僅是一個對象變量,沒有引用對象,它也不是一個對象,所以不能
//調用Date類的方法,就好比一個變量僅僅聲明卻沒有初始化,所以不能使用。
//初始化有兩種
date2=new Date();
date2=date;//date,date2引用同一變量
對象變量只是引用對象,並不是包含對象。
java中任何一個變量都是對存儲在另外一個地方的對象的引用。
Date date=new Date();
new Date()返回一個Date對象的引用,這個引用存在了date變量中。
如果把一個對象變量設置爲空,表明此時變量沒有引用任何對象。
date=null;
和c++不同
c++中引用不能爲空,且不能被賦值。其實java中的引用類似與c++中的指針。
自定義類
首先,自定義一個類
public class EmployeeTest
{
public static void main(String[] args)
{
Employee[] staff=new Employe[3];
staff[0]=new Employee("zhang",5000,2016,5,1);
staff[1]=new Employee("li",5000,2016,5,1);
staff[2]=new Employee("wang",5000,2016,5,1);
for(Employee e:staff)
{
e.raiseSalary(5);
}
for(Employee e:staff)
{
System.out.println("name="+e.getName()+",salary="+e.getSalary()+",hireDay="+e.getHireDay());
}
}
class Employee
{
//構造器
public Employee(String n,double s,int year,int month,int day)
{
name=n;
salary=s;
GregorianCalendar calendar=new GregorianCalendar(year,month-1,day);
hireDay=calendar.getTime();
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public void raiseSalary(double byPercent)
{
double raise=salary*byPercent/100;
salary+=raise;
}
private String name;
private double salary;
private Date hireDay;
}
}
構造器
public Employee(String n,double s,int year,int month,int day)
{
name=n;
salary=s;
GregorianCalendar calendar=new GregorianCalendar(year,month-1,day);
hireDay=calendar.getTime();
}
構造器與類同名,在構造(實例化)對象時,構造器被執行,初始化實例域。
new Employee("zhang",5000,2016,5,1);
構造器只能被new,而不能用當做方法來調用
staff.Employee("zhang",5000,2016,5,1);//錯誤
Note:
構造器與類同名
每個類可以有一個以上的構造器
構造器可以有0個或0個以上的參數
構造器沒有返回值
構造器前面加new關鍵字,不能被對象調用
public Employee(String n,double s,int year,int month,int day)
{
String name=n;//錯誤
double salary=s;//錯誤
...
}
局部變量會覆蓋掉實例域。
封裝的優點
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public Date getHireDay()
{
return hireDay;
}
這些都是典型的訪問器方法。由於它們只返回實例域值,因此又被稱爲域訪問器。將name、 salary和hireDay域標記爲public,以此來取代獨立的訪問器方法會不會更容易些呢?
關鍵在於name是一個只讀域。一旦在構造器中設置完畢,就沒有任何一個辦法可以對它進行修改,這樣來確保name域不會受到外界的干擾。
雖然salary不是隻讀域,但是它只能用raiseSalary方法修改。特別是一旦這個域值出現了錯誤,只要調試這個方法就可以了。如果salary域public的,破壞這個域值的搗亂者有可能會出沒在任何地方。
在有些時候,需要獲得或設置實例域的值。因此,應該提供下面三項內容:
• 一個私有的數據域;
• 一個公有的域訪問器方法;
• 一個公有的域更改器方法。
這樣做要比提供一個簡單的公有數據域複雜些,但是卻有着下列明顯的好處:
1)可以改變內部實現,除了該類的方法之外,不會影響其他代碼。
例如,如果將存儲名字的域改爲:
String firstName;
String lastName;
getName方法改爲
return firstName+" "+lastName;
對於這點改變,程序的其他部分完全不可見。
當然,爲了進行新舊數據表示之間的轉換,訪問器方法和更改器方法有可能需要做許多工作。但是,這將爲我們帶來了第二點好處。
2)更改器方法可以執行錯誤檢查,然而直接對域進行賦值將不會進行這些處理。
例如, setSalary方法可以檢查薪金是否小於0。
final實例域
可以將實例域定義爲final。構建對象時必須初始化這樣的域。也就是說,必須確保在每一個構造器執行之後,這個域的值被設置,並且在後面的操作中,不能夠再對它進行修改。例如,可以將Employee類中的name域聲明爲final,因爲在對象構建之後,這個值不會再被修改,即沒有setName方法。
class Employee
{
private final String name;
...
}
final修飾符大都應用於基本數據( primitive)類型域,或不可變( immutable)類的域(如果類中的每個方法都不會改變其對象,這種類就是不可變的類。例如, String類就是一個不可變的類)。對於可變的類,使用final修飾符可能會對讀者造成混亂。例如,
private final Date date;
僅僅意味着存儲在date變量中的對象引用在對象構造之後不能被改變,而並不意味着date 對象是一個常量。任何方法都可以對date引用的對象調用setTime更改器。
靜態域與靜態方法
在前面給出的示例程序中, main方法都被標記爲static修飾符。下面討論一下這個修飾符的含義。
靜態域(類域)
如果將域定義爲static,每個類中只有一個這樣的域。而每一個對象對於所有的實例域卻都有自己的一份拷貝。例如,假定需要給每一個僱員賦予惟一的標識碼。這裏給Employee類添加一個實例域id和一個靜態域nextId:
class Employee
{
private int id;
private static int nextId=1;
}
現在,每一個僱員對象都有一個自己的id域,但這個類的所有實例將共享一個nextId域。換句話說,如果有1000個Employee類的對象,則有1000個實例域id。但是,只有一個靜態域nextId。即使沒有一個僱員對象,靜態域nextId也存在。它屬於類,而不屬於任何獨立的對象。
靜態常量
靜態變量使用得比較少,但靜態常量卻使用得比較多。例如,在Math類中定義了一個靜態常量:
public class Math
{
public static final double PI=3.1415...;
...
}
在程序中,可以採用Math.PI的形式獲得這個常量。
如果關鍵字static被省略, PI就變成了Math類的一個實例域。需要通過Math類的對象訪問PI,並且每一個Math對象都有它自己的一份PI拷貝。
另一個多次使用的靜態常量是System.out。它在System類中聲明:
public class System
{
public static final PrintStream out=...;
}
前面曾經提到過,由於每個類對象都可以對公有域進行修改,所以,最好不要將域設計爲public。然而,公有常量(即final域)卻沒問題。因爲out被聲明爲final,所以,不允許再將其他打印流賦給它:
System.out=new PrintStream();//錯,out is final
靜態方法
靜態方法是一種不能向對象實施操作的方法。例如, Math類的pow方法就是一個靜態方法。表達式Math.pow(x,a)
計算冪。在運算時,不使用任何Math對象。換句話說,沒有隱式的參數。
可以認爲靜態方法是沒有this參數的方法(在一個非靜態的方法中, this參數表示這個方法的隱式參數)。
因爲靜態方法不能操作對象,所以不能在靜態方法中訪問實例域。但是,靜態方法可以訪問自身類中的靜態域。下面是使用這種靜態方法的一個示例:
public static int getNextId()
{
return nextId;//nextId is static field
}
可以通過類名調用這個方法:
int id=Employee.getNextId();
這個方法可以省略關鍵字static嗎?答案是肯定的。但是,需要通過Employee類對象的引用調用這個方法。
Note:
可以使用對象調用靜態方法。例如,如果harry是一個Employee對象,可以用
harry.getNextId( )(或者this.getNextId(),或省略this,直接getNextId())代替Employee.getnextId( )。不過,這種方式很容易造成混淆,其原因是getNextId方法計算的結果與harry毫無關係。我們建議使用類名,而不是對象來調用靜態方法。
在下面兩種情況下使用靜態方法:
• 一個方法不需要訪問對象狀態,其所需參數都是通過顯式參數提供(例如: Math.pow)。
• 一個方法只需要訪問類的靜態域(例如: Employee.getNextId)
main方法
不需要使用對象調用靜態方法。例如,不需要構造Math類對象就可以調用Math.pow。
main方法也是一個靜態方法。main方法不對任何對象進行操作。事實上,在啓動程序時還沒有任何一個對象。靜態的main方法將執行並創建程序所需要的對象。
Note:
每一個類可以有一個main方法。這是一個常用於對類進行單元測試的技巧。
方法參數
參數傳遞給方法(函數)的兩種形式:
值調用( call by value):表示方法接收的是調用者提供的值。
而引用調用( call by reference):表示方法接收的是調用者提供的變量地址。
Note:
一個方法可以修改傳遞引用所對應的變量值,而不能修改傳遞值調用所對應的變量值。
Java程序設計語言總是採用值調用。也就是說,方法得到的是所有參數值的一個拷貝,特別是,方法不能修改傳遞給它的任何參數變量的內容。
例如,考慮下面的調用:
double percent=10;
harry.raiseSalary(percent);
不必理睬這個方法的具體實現,在方法調用之後, percent的值還是10。
下面再仔細地研究一下這種情況。假定一個方法試圖將一個參數值增加至3倍:
public static void tripleValue(double x)
{
x=x*3;
}
調用這個方法
double percent=10;
tripleValue(percent);
percent的值還是10,看一下執行過程
1) x被初始化爲percent值的一個拷貝(也就是10)。
2) x被乘以3後等於30。但是percent仍然是10(如圖)。
3)這個方法結束之後,參數變量x不再使用。
然而,參數類型有兩種
• 基本數據類型 (數字、布爾值)。
• 對象引用。
一個方法不可能修改一個基本數據類型的參數。而對象引用作爲參數就不
同了,可以很容易地利用下面這個方法實現將一個僱員的薪金提高兩倍的操作:
public static void tripleSalary(Employee x)
{
x.raiseSalary(200);
}
當調用
harry=new Employee(...);
tripleSalary(harry);
時,具體的執行過程爲:
1) x被初始化爲harry值的拷貝,這裏是一個對象的引用。
2) raiseSalary方法應用於這個對象引用。 x和harry同時引用的那個Employee對象的薪金提高了200%。
3)方法結束後,參數變量x不再使用。當然,對象變量harry繼續引用那個薪金增至3倍的僱員對象(如圖4-7所示)。
誤區:引用調用
首先,編寫一個交換兩個僱員對象的方法
public static void swap(Employee x,Employee y)
{
Employee temp=x;
x=y;
y=temp;
}
如果Java程序設計語言對對象採用的是引用調用,那麼這個方法就應該能夠實現交換數據的效果:
Employee a=new Employee("A",...);
Employee b=new Employee("B",...);
swap(a,b);
但是,方法並沒有改變存儲在變量a和b中的對象引用。 swap方法的參數x和y被初始化爲兩個對象引用的拷貝,這個方法交換的是這兩個拷貝。相當於
//x refers to A,y to B
Employee temp=x;
x=y;
y=temp;
//now x refers to B,y to A
最終,白費力氣。在方法結束時參數變量x和y被丟棄了。原來的變量a和b仍然引用這個方法調用之前所引用的對象(如圖4-8所示)。
這個過程說明: Java程序設計語言對對象採用的不是引用調用,實際上, 對象引用進行的是值傳遞。
下面總結一下在Java程序設計語言中,方法參數的使用情況:
• 一個方法不能修改一個基本數據類型的參數(即數值型和布爾型)。
• 一個方法可以改變一個對象參數的狀態。
• 一個方法不能實現讓對象參數引用一個新的對象。
對象構造
對象重載
Employee staff=new Employee();
Employee staff1=new Employee(...);
重載( overloading):如果多個方法,有相同的名字、不同的參數,便產生了重載。
Note
Java允許重載任何方法,而不只是構造器方法。因此,要完整地描述一個方法,需要指出方法名以及參數類型。這叫做方法的簽名( signature)。例如, String類有4個稱爲indexOf的公有方法。它們的簽名是
indexOf(int)
indexOf(int,int)
indexOf(String)
indexOf(String,int)
返回類型不是方法簽名的一部分。也就是說,不能有兩個名字相同、參數類型也相同卻返回不同類型值的方法。
默認域初始化
如果在構造器中沒有顯式地給域賦予初值,那麼就會被自動地賦爲默認值:數值爲0、布爾值爲flase、對象引用爲null。然而,只有缺少程序設計經驗的人才會這樣做。確實,如果不明確地對域進行初始化,就會影響程序代碼的可讀性。
Note:
這是域與局部變量的主要不同點。必須明確地初始化方法中的局部變量。但是,
如果沒有初始化類中的域,將會被初始化爲默認值( 0、 false或null)。
例如,仔細看一下Employee類。假定沒有在構造器中對某些域進行初始化,就會默認地將salary域初始化爲0,將name、 hireDay域初始化爲null。
但是,這並不是一種良好的編程習慣。如果此時調用getName方法或getHireDay方法,則會得到一個null引用,這應該不是我們所希望的結果:
harry=new Employee();//默認構造方法,域初始化默認值
Date date=harry.getHireDay();
calendar.setTime(date);//空指針異常
默認構造器
所謂默認構造器是指沒有參數的構造器。例如, Employee類的默認構造器:
public Employee()
{
name="";
salary=0;
hireDay=new Date();
}
如果在編寫一個類時沒有編寫構造器,那麼系統就會提供一個默認構造器。這個默認構造器將所有的實例域設置爲默認值。於是,實例域中的數值型數據設置爲0、布爾型數據設置爲false、所有對象變量將設置爲null。
如果類中提供了至少一個構造器,但是沒有提供默認的構造器,則在構造對象時如果沒有提供構造參數就會被視爲不合法。例如,在例4-2中的Employee類提供了一個簡單的構造器:
Employee(String name,double salary,int y,int m,int d)
對於這個類,構造默認的僱員屬於不合法。也就是,調用
e=new Employee();
將會產生錯誤。
Note
請記住,僅當類沒有提供任何構造器的時候,系統纔會提供一個默認的構造器。
如果在編寫類的時候,給出了一個構造器,哪怕是很簡單的,要想讓這個類的用戶能夠採用下列方式構造實例:
new ClassName()
就必須提供一個默認的構造器(即不帶參數的構造器)。當然,如果希望所有域被賦予默認值,可以採用下列格式:
public ClassName()
{
}
顯式域初始化
由於類的構造器方法可以重載,所以可以採用多種形式設置類的實例域的初始狀態。確保不管怎樣調用構造器,每個實例域都可以被設置爲一個有意義的初值。這是一種很好的設計習慣。可以在類定義中,直接將一個值賦給任何域。例如:
class Employee
{
...
private String name="";
...
}
在執行構造器之前,先執行賦值操作。當一個類的所有構造器都希望把相同的值賦予某個特定的實例域時,這種方式特別有用。
初始值不一定是常量。在下面的例子中,可以調用方法對域進行初始化。仔細看一下Employee類,其中每個僱員有一個id域。可以使用下列方式進行初始化:
class Employee
{
...
public int assignId()
{
int r=nextId;
nextId++;
return r;
}
...
private int id=assignId();
}
Note:
在C++中,不能直接初始化實例域。所有的域必須在構造器中設置。但是,有
一個特殊的初始化器列表語法,如下所示:
Employee:Employee(String name,double salary,int ,int m,int d):name(n),salary(s),hireDay(y,m,d)
{
}
C++使用這種特殊的語法來調用域構造器。在Java中沒有這種必要,因爲對象沒有子對象,只有指向其他對象的指針。
調用另一個構造器(this使用技巧)
關鍵字this引用方法的隱式參數。然而,這個關鍵字還有另外一個含義。
如果構造器的第一個語句形如this(…),這個構造器將調用同一個類的另一個構造器。下面是一個典型的例子:
public Employee(double s)
{
this("Employee#"+nextId,s)
nextId++;
}
當調用new Employee(60000)時, Employee(double)構造器將調用Employee(String, double)構造器。
初始化塊
前面已經講過兩種初始化數據域的方法:
• 在構造器中設置值
• 在聲明中賦值
實際上, Java還有第三種機制,稱爲初始化塊( initialization block)。在一個類的聲明中,可以包含多個代碼塊。只要構造類的對象,這些塊就會被執行。例如,
class Employee
{
public Employee(String n,double s)
{
name=n;
salary=s;
}
public Employee()
{
name="";
salary=0;
}
...
private static int nextId;
private int id;
private String name;
private double salary;
...
//initialization block
{
id=nextId;
nextId++;
}
}
在這個示例中,無論使用哪個構造器構造對象, id域都在對象初始化塊中被初始化。首先運行初始化塊,然後才運行構造器的主體部分。
這種機制不是必須的,也不常見。通常,直接將初始化代碼放在構造器中。
即使域定義在類的後半部分,在初始化塊中仍然可以爲它設置值。但是,爲了避免循環定義,不要讀取在後面初始化的域。
由於初始化數據域有多種途徑,所以列出構造過程的所有路徑可能相當混亂。下面是調用構造器的具體處理步驟:
1)所有數據域被初始化爲默認值( 0、 false或null)。
2)按照在類聲明中出現的次序,依次執行所有域初始化語句和初始化塊。
3)如果構造器第一行調用了第二個構造器,則執行第二個構造器主體。
4)執行這個構造器的主體。
當然,應該精心地組織好初始化代碼,這樣有利於其他程序員的理解。例如,如果讓類的構造器行爲依賴於數據域聲明的順序,
那就會顯得很奇怪並且容易引起錯誤。可以通過提供一個初始化值,或者使用一個靜態的初始化塊來對靜態域進行初始化。前面已經介紹過第一種機制:
static int nextId=1;
如果對類的靜態域進行初始化的代碼比較複雜,那麼可以使用靜態的初始化塊。
將代碼放在一個塊中,並標記關鍵字static。
下面是一個示例。其功能是將僱員ID的起始值賦予一個小於10 000的隨機整數。
static{
Random generator=new Random();
nextId=generator.nextId(10000);
}
在類第一次加載的時候,將會進行靜態域的初始化。與實例域一樣,除非將它們顯式地設置成其他值,否則默認的初始值是 0、 false或null。
所有的靜態初始化語句以及靜態初始化塊都將依照類定義的順序執行。
上面說了那麼多,到底實例域、靜態域代碼塊、代碼塊、構造函數執行順序是怎樣的?
public class HelloA {
public HelloA(){
System.out.println("HelloA構造函數執行了");
}
{
System.out.println("HelloA代碼塊執行了");
}
static
{
System.out.println("HelloA static代碼塊執行了");
}
//private String str="HelloA實例域執行了";
}
public class HelloB extends HelloA {
public HelloB() {
this(s);
System.out.println("HelloB構造函數執行了");
}
public HelloB(String s) {
System.out.println("HelloB(String)構造函數執行了"+s);
}
{
System.out.println("HelloB代碼塊執行了");
}
static {
s="hhhhh";
System.out.println("HelloB static代碼塊執行了");
}
public static void main(String[] args) {
new HelloB();
}
private static String s;
}
/*
* 其中涉及:靜態初始化代碼塊、構造代碼塊、構造方法 當涉及到繼承時,按照如下順序執行:
* 1、執行父類的靜態代碼塊
* static {
* System.out.println("static A");
* }
* 輸出:static A
* 2、執行子類的靜態代碼塊
* static {
* System.out.println("static B");
* }
* 輸出:static B
* 3、執行父類的構造代碼塊
* {
* System.out.println("I’m A class");
* }
* 輸出:I'm A class
* 4、執行父類的構造函數
* public HelloA() { }
* 輸出:無
* 5、執行子類的構造代碼塊
* { System.out.println("I’m B class"); }
* 輸出:I'm B class
* 6、執行子類的構造函數
* public HelloB() { }
* 輸出:無
*
* 那麼,最後的輸出爲: static A static B I'm A class I'm B class
*/
對象析構與finalize方法
Java有自動的垃圾回收器,不需要人工回收內存,所以Java不支持析構器。
當然,某些對象使用了內存之外的其他資源,例如,文件或使用了系統資源的另一個對象的句柄。在這種情況下,當資源不再需要時,將其回收和再利用將顯得十分重要。
可以爲任何一個類添加finalize方法。 finalize方法將在垃圾回收器清除對象之前調用。在實際應用中,不要依賴於使用finalize方法回收任何短缺的資源,這是因爲很難知道這個方法什麼時候才能夠調用。