類
上節我們介紹了函數調用的基本原理,本節和接下來幾節,我們探索類的世界。
程序主要就是數據以及對數據的操作,為方便理解和操作,高級語言使用數據類型這個概念,不同的數據類型有不同的特征和操作,Java定義了八種基本數據類型,其中,四種整形byte/short/int/long,兩種浮點類型float/double,一種真假類型boolean,一種字符類型char,其他類型的數據都用類這個概念表達。
前兩節我們暫時將類看做函數的容器,在某些情況下,類也確實基本上只是函數的容器,但類更多表示的是自定義數據類型,我們先從容器的角度,然後從自定義數據類型的角度談談類。
函數容器
我們看個例子,Java API中的類Math,它裡面主要就包含了若干數學函數,下表列出了其中一些:
Math函數
功能
int round(float a)
四設五入
double sqrt(double a)
平方根
double ceil(double a)
向上取整
double floor(double a)
向下取整
double pow(double a, double b)
a的b次方
int abs(int a)
絕對值
int max(int a, int b)
最大值
double log(double a)
自然對數
double random()
產生一個大於等於0小於1的隨機數
使用這些函數,直接在前面加Math.即可,例如Math.abs(-1)返回1。
這些函數都有相同的修飾符,public static。
static表示類方法,也叫靜態方法,與類方法相對的是實例方法。實例方法沒有static修飾符,必須通過實例或者叫對象(待會介紹)調用,而類方法可以直接通過類名進行調用的,不需要創建實例。
public表示這些函數是公開的,可以在任何地方被外部調用。與public相對的有private, 如果是private,表示私有,這個函數只能在同一個類內被別的函數調用,而不能被外部的類調用。在Math類中,有一個函數 Random initRNG()就是private的,這個函數被public的方法random()調用以生成隨機數,但不能在Math類以外的地方被調用。
將函數聲明為private可以避免該函數被外部類誤用,調用者可以清楚的知道哪些函數是可以調用的,哪些是不可以調用的。類實現者通過private函數封裝和隱藏內部實現細節,而調用者只需要關心public的就可以了。可以說,通過private封裝和隱藏內部實現細節,避免被誤操作,是計算機程序的一種基本思維方式。
除了Math類,我們再來看一個例子Arrays,Arrays裡面包含很多與數組操作相關的函數,下表列出了其中一些:
Arrays函數
功能
void sort(int[] a)
排序,按升序排,整數數組
void sort(double[] a)
排序,按升序排,浮點數數組
int binarySearch(long[] a, long key)
二分查找,數組已按升序排列
void fill(int[] a, int val)
給所有數組元素賦相同的值
int[] copyOf(int[] original, int newLength)
數組拷貝
boolean equals(char[] a, char[] a2)
判斷兩個數組是否相同
這裡將類看做函數的容器,更多的是從語言實現的角度看,從概念的角度看,Math和Arrays也可以看做是自定義數據類型,分別表示數學和數組類型,其中的public static函數可以看做是類型能進行的操作。接下來讓我們更為詳細的討論自定義數據類型。
自定義數據類型
我們將類看做自定義數據類型,所謂自定義數據類型就是除了八種基本類型以外的其他類型,用於表示和處理基本類型以外的其他數據。
一個數據類型由其包含的屬性以及該類型可以進行的操作組成,屬性又可以分為是類型本身具有的屬性,還是一個具體數據具有的屬性,同樣,操作也可以分為是類型本身可以進行的操作,還是一個具體數據可以進行的操作。
這樣,一個數據類型就主要由四部分組成:
不過,對於一個具體類型,每一個部分不一定都有,Arrays類就只有類方法。
類變量和實例變量都叫成員變量,也就是類的成員,類變量也叫靜態變量或靜態成員變量。類方法和實例方法都叫成員方法,也都是類的成員,類方法也叫靜態方法。
類方法我們上面已經看過了,Math和Arrays類中定義的方法就是類方法,這些方法的修飾符必須有static。下面解釋下類變量,實例變量和實例方法。
類變量
類型本身具有的屬性通過類變量體現,經常用於表示一個類型中的常量,比如Math類,定義了兩個數學中常用的常量,如下所示:
public static final double E = 2.7182818284590452354; public static final double PI = 3.14159265358979323846;
E表示數學中自然對數的底數,自然對數在很多學科中有重要的意義,PI表示數學中的圓周率π。與類方法一樣,類變量可以直接通過類名訪問,如Math.PI。
這兩個變量的修飾符也都有public static,public表示外部可以訪問,static表示是類變量。與public相對的主要也是private,表示變量只能在類內被訪問。與static相對的是實例變量,沒有static修飾符。
這裡多了一個修飾符final,final 在修飾變量的時候表示常量,即變量賦值後就不能再修改了。使用final可以避免誤操作,比如說,如果有人不小心將Math.PI的值改了,那麼很多相關的計算就會出錯。另外,Java編譯器可以對final變量進行一些特別的優化。所以,如果數據賦值後就不應該再變了,就加final修飾符吧。
表示類變量的時候,static修飾符是必需的,但public和final都不是必需的。
實例變量和實例方法
實例字面意思就是一個實際的例子,實例變量表示具體的實例所具有的屬性,實例方法表示具體的實例可以進行的操作。如果將微信訂閱號看做一個類型,那"老馬說 編程"訂閱號就是一個實例,訂閱號的頭像、功能介紹、發布的文章可以看做實例變量,而修改頭像、修改功能介紹、發布新文章可以看做實例方法。與基本類型對 比,int a;這個語句,int就是類型,而a就是實例。
接下來,我們通過定義和使用類,來進一步理解自定義數據類型。
定義第一個類
我們定義一個簡單的類,表示在平面坐標軸中的一個點,代碼如下:
class Point { public int x; public int y; public double distance(){ return Math.sqrt(x*x+y*y); } }
我們來解釋一下:
public class Point
表示類型的名字是Point,是可以被外部公開訪問的。這個public修飾似乎是多余的,不能被外部訪問還能有什麼用?在這裡,確實不能用private 修飾Point。但修飾符可以沒有(即留空),表示一種包級別的可見性,我們後續章節介紹,另外,類可以定義在一個類的內部,這時可以使用private 修飾符,我們也在後續章節介紹。
public int x; public int y;
定義了兩個實例變量,x和y,分別表示x坐標和y坐標,與類變量類似,修飾符也有public或private修飾符,表示含義類似,public表示可被外部訪問,而private表示私有,不能直接被外部訪問,實例變量不能有static修飾符。
public double distance(){ return Math.sqrt(x*x+y*y); }
定義了實例方法distance,表示該點到坐標原點的距離。該方法可以直接訪問實例變量x和y,這是實例方法和類方法的最大區別,實例方法和類方法都可以有參數、操作、返回值,但類方法只能操作參數和方法內定義的變量,而實例方法可以直接操作實例變量,可以讀也可以改。
實例方法直接訪問實例變量,到底是什麼意思呢?其實,在實例方法中,有一個隱含的參數,這個參數就是當前操作的實例自己,直接操作實例變量,實際也需要通過參數進行。後續我們還會進一步解釋。
使用第一個類
定義了類本身和定義了一個函數類似,本身不會做什麼事情,不會分配內存,也不會執行代碼。方法要執行需要被調用,而實例方法被調用,首先需要一個實例,實例也稱為對象,我們可能會交替使用。下面的代碼演示了如何使用:
public static void main(String[] args) { Point p = new Point(); p.x = 2; p.y = 3; System.out.println(p.distance()); }
我們解釋一下:
Point p = new Point();
這個語句包含了Point類型的變量聲明和賦值,它可以分為兩部分:
1 Point p; 2 p = new Point();
Point p聲明了一個變量,這個變量叫p,是Point類型的。這個變量和數組變量是類似的,都有兩塊內存,一塊存放實際內容,一塊存放實際內容的位置。聲明變量本身只會分配存放位置的內存空間,這塊空間還沒有指向任何實際內容。因為這種變量和數組變量本身不存儲數據,而只是存儲實際內容的位置,它們也都稱為引用類型的變量。
p = new Point();創建了一個實例或對象,然後賦值給了Point類型的變量p,它至少做了兩件事:
與方法內定義的局部變量不同,在創建對象的時候,所有的實例變量都會分配一個默認值,這與在創建數組的時候是類似的,數值類型變量的默認值是 0,boolean是false, char是'\u0000',引用類型變量都是null,null是一個特殊的值,表示不指向任何對象。這些默認值可以修改,我們待會介紹。
p.x = 2; p.y = 3;
給對象的變量賦值,語法形式是:對象變量名.成員名。
System.out.println(p.distance());
調用實例方法distance,並輸出結果,語法形式是:對象變量名.方法名。實例方法內對實例變量的操作,實際操作的就是p這個對象的數據。
我們在介紹基本類型的時候,是先定義數據,然後賦值,最後是操作,自定義類型與此類似:
可以看出,對實例變量和實例方法的訪問都通過對象進行,通過對象來訪問和操作其內部的數據是一種基本的面向對象思維。本例中,我們通過對象直接操作了其內部數據x和y,這是一個不好的習慣,一般而言,不應該將實例變量聲明為public,而只應該通過對象的方法對實例變量進行操作,原因也是為了減少誤操作,直接訪問變量沒有辦法進行參數檢查和控制,而通過方法修改,可以在方法中進行檢查。
修改變量默認值
之前我們說,實例變量都有一個默認值,如果希望修改這個默認值,可以在定義變量的同時就賦值,或者將代碼放入初始化代碼塊中,代碼塊用{}包圍,如下面代碼所示:
int x = 1; int y; { y = 2; }
x的默認值設為了1,y的默認值設為了2。在新建一個對象的時候,會先調用這個初始化,然後才會執行構造方法中的代碼。
靜態變量也可以這樣初始化:
static int STATIC_ONE = 1; static int STATIC_TWO; { STATIC_TWO = 2; }
修改類 - 實例變量改為private
上面我們說一般不應該將實例變量聲明為public,下面我們修改一下類的定義,將實例變量定義為private,通過實例方法來操作變量,代碼如下:
class Point { private int x; private int y; public void setX(int x) { this.x = x; } public void setY(int y) { this.y = y; } public int getX() { return x; } public int getY() { return y; } public double distance() { return Math.sqrt(x * x + y * y); } }
這個定義中,我們加了四個方法,setX/setY用於設置實例變量的值,getX/getY用於獲取實例變量的值。
這裡面需要介紹的是this這個關鍵字,this表示當前實例, 在語句this.x=x;中,this.x表示實例變量x,而右邊的x表示方法參數中的x。前面我們提到,在實例方法中,有一個隱含的參數,這個參數就是this,沒有歧義的情況下,可以直接訪問實例變量,在這個例子中,兩個變量名都叫x,則需要通過加上this來消除歧義。
這四個方法看上去是非常多余的,直接訪問變量不是更簡潔嗎?而且上節我們也說過,函數調用是有成本的。在這個例子中,意義確實不太大,實際上,Java編譯器一般也會將對這幾個方法的調用轉換為直接訪問實例變量,而避免函數調用的開銷。但在很多情況下,通過函數調用可以封裝內部數據,避免誤操作,我們一般還是不將成員變量定義為public。
使用這個類的代碼如下:
public static void main(String[] args) { Point p = new Point(); p.setX(2); p.setY(3); System.out.println(p.distance()); }
將對實例變量的直接訪問改為了方法調用。
修改類 - 引入構造方法
在初始化對象的時候,前面我們都是直接對每個變量賦值,有一個更簡單的方式對實例變量賦初值,就是構造方法,我們先看下代碼,在Point類定義中增加如下代碼:
public Point(){ this(0,0); } public Point(int x, int y){ this.x = x; this.y = y; }
這兩個就是構造方法,構造方法可以有多個。不同於一般方法,構造方法有一些特殊的地方:
與普通方法一樣,構造方法也可以重載。第二個構造方法是比較容易理解的,使用this對實例變量賦值。
我們解釋下第一個構造方法,this(0,0)的意思是調用第二個構造方法,並傳遞參數0,0,我們前面解釋說this表示當前實例,可以通過this訪問實例變量,這是this的第二個用法,用於在構造方法中調用其他構造方法。
這個this調用必須放在第一行,這個規定應該也是為了避免誤操作,構造方法是用於初始化對象的,如果要調用別的構造方法,先調別的,然後根據情況自己再做調整,而如果自己先初始化了一部分,再調別的,自己的修改可能就被覆蓋了。
這個例子中,不帶參數的構造方法通過this(0,0)又調用了第二個構造方法,這個調用是多余的,因為x和y的默認值就是0,不需要再單獨賦值,我們這裡主要是演示其語法。
我們來看下如何使用構造方法,代碼如下:
Point p = new Point(2,3);
這個調用就可以將實例變量x和y的值設為2和3。前面我們介紹 new Point()的時候說,它至少做了兩件事,一個是分配內存,另一個是給實例變量設置默認值,這裡我們需要加上一件事,就是調用構造方法。調用構造方法是new操作的一部分。
通過構造方法,可以更為簡潔的對實例變量進行賦值。
默認構造方法
每個類都至少要有一個構造方法,在通過new創建對象的過程中會被調用。但構造方法如果沒什麼操作要做,可以省略。Java編譯器會自動生成一個默認構造方 法,也沒有具體操作。但一旦定義了構造方法,Java就不會再自動生成默認的,具體什麼意思呢?在這個例子中,如果我們只定義了第二個構造方法(帶參數的),則下面語句:
Point p = new Point();
就會報錯,因為找不到不帶參數的構造方法。
為什麼Java有時候幫助自動生成,有時候不生成呢?你在沒有定義任何構造方法的時候,Java認為你不需要,所以就生成一個空的以被new過程調用,你定義了構造方法的時候,Java認為你知道自己在干什麼,認為你是有意不想要不帶參數的構造方法的,所以不會幫你生成。
私有構造方法
構造方法可以是私有方法,即修飾符可以為private, 為什麼需要私有構造方法呢?大概可能有這麼幾種場景:
關鍵字小結
本節我們提到了多個關鍵字,這裡匯總一下:
類和對象的生命周期
類
在程序運行的時候,當第一次通過new創建一個類的對象的時候,或者直接通過類名訪問類變量和類方法的時候,Java會將類加載進內存,為這個類型分配一塊空間,這個空間會包括類的定義,它有哪些變量,哪些方法等,同時還有類的靜態變量,並對靜態變量賦初始值。
類加載進內存後,一般不會釋放,直到程序結束。一般情況下,類只會加載一次,所以靜態變量在內存中只有一份。
對象
當通過new創建一個對象的時候,對象產生,在內存中,會存儲這個對象的實例變量值,每new一次,對象就會產生一個,就會有一份獨立的實例變量。
每個對象除了保存實例變量的值外,可以理解還保存著對應類型即類的地址,這樣,通過對象能知道它的類,訪問到類的變量和方法代碼。
實例方法可以理解為一個靜態方法,只是多了一個參數this,通過對象調用方法,可以理解為就是調用這個靜態方法,並將對象作為參數傳給this。
對象的釋放是被Java用垃圾回收機制管理的,大部分情況下,我們不用太操心,當對象不再被使用的時候會被自動釋放。
具體來說,對象和數組一樣,有兩塊內存,保存地址的部分分配在棧中,而保存實際內容的部分分配在堆中。棧中的內存是自動管理的,函數調用入棧就會分配,而出棧就會釋放。
堆中的內存是被垃圾回收機制管理的,當沒有活躍變量指向對象的時候,對應的堆空間就可能被釋放,具體釋放時間是Java虛擬機自己決定的。活躍變量,具體的說,就是已加載的類的類變量,和棧中所有的變量。
小結
本 節我們主要從自定義數據類型的角度介紹了類,談了如何定義類,以及如何創建對象,如何使用類。自定義類型由類變量、類方法、實例變量和實例方法組成,為方 便對實例變量賦值,介紹了構造方法。本節引入了多個關鍵字,我們介紹了這些關鍵字的含義。最後我們介紹了類和對象的生命周期。
通過類實現自定義數據類型,封裝該類型的數據所具有的屬性和操作,隱藏實現細節,從而在更高的層次上(類和對象的層次,而非基本數據類型和函數的層次)考慮和操作數據,是計算機程序解決復雜問題的一種重要的思維方式。
本節介紹的Point類,其屬性只有基本數據類型,下節我們介紹類的組合,以表達更為復雜的概念。
----------------
未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。原創文章,保留所有版權。