【轉載】計算機程序的思維邏輯 (13) - 類
類
上節我們介紹了函數調用的基本原理,本節和接下來幾節,我們探索類的世界。
程序主要就是數據以及對數據的操作,為方便理解和操作,高級語言使用數據類型這個概念,不同的數據類型有不同的特征和操作,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,它至少做了兩件事:
- 分配內存,以存儲新對象的數據,對象數據包括這個對象的屬性,具體包括其實例變量x和y。
- 給實例變量設置默認值,int類型默認值為0。
與方法內定義的局部變量不同,在創建對象的時候,所有的實例變量都會分配一個默認值,這與在創建數組的時候是類似的,數值類型變量的默認值是 0,boolean是false, char是‘\u0000‘,引用類型變量都是null,null是一個特殊的值,表示不指向任何對象。這些默認值可以修改,我們待會介紹。
p.x = 2; p.y = 3;
給對象的變量賦值,語法形式是:對象變量名.成員名。
System.out.println(p.distance());
調用實例方法distance,並輸出結果,語法形式是:對象變量名.方法名。實例方法內對實例變量的操作,實際操作的就是p這個對象的數據。
我們在介紹基本類型的時候,是先定義數據,然後賦值,最後是操作,自定義類型與此類似:
- Point p = new Point(); 是定義數據並設置默認值
- p.x = 2; p.y = 3; 是賦值
- p.distance() 是數據的操作
可以看出,對實例變量和實例方法的訪問都通過對象進行,通過對象來訪問和操作其內部的數據是一種基本的面向對象思維。本例中,我們通過對象直接操作了其內部數據x和y,這是一個不好的習慣,一般而言,不應該將實例變量聲明為public,而只應該通過對象的方法對實例變量進行操作,原因也是為了減少誤操作,直接訪問變量沒有辦法進行參數檢查和控制,而通過方法修改,可以在方法中進行檢查。
修改變量默認值
之前我們說,實例變量都有一個默認值,如果希望修改這個默認值,可以在定義變量的同時就賦值,或者將代碼放入初始化代碼塊中,代碼塊用{}包圍,如下面代碼所示:
int x = 1; int y; { y = 2; }
x的默認值設為了1,y的默認值設為了2。在新建一個對象的時候,會先調用這個初始化,然後才會執行構造方法中的代碼。
靜態變量也可以這樣初始化:
static int STATIC_ONE = 1; static int STATIC_TWO; static { STATIC_TWO = 2; }
STATIC_TWO=2;語句外面包了一個 static {},這叫靜態初始化代碼塊。靜態初始化代碼塊在類加載的時候執行,這是在任何對象創建之前,且只執行一次。
修改類 - 實例變量改為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; }
這兩個就是構造方法,構造方法可以有多個。不同於一般方法,構造方法有一些特殊的地方:
- 名稱是固定的,與類名相同。這也容易理解,靠這個用戶和Java系統就都能容易的知道哪些是構造方法。
- 沒有返回值,也不能有返回值。這個規定大概是因為返回值沒用吧。
與普通方法一樣,構造方法也可以重載。第二個構造方法是比較容易理解的,使用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, 為什麽需要私有構造方法呢?大概可能有這麽幾種場景:
- 不能創建類的實例,類只能被靜態訪問,如Math和Arrays類,它們的構造方法就是私有的。
- 能創建類的實例,但只能被類的的靜態方法調用。有一種常用的場景,即類的對象有但是只能有一個,即單例模式(後續文章介紹),在這個場景中,對象是通過靜態方法獲取的,而靜態方法調用私有構造方法創建一個對象,如果對象已經創建過了,就重用這個對象。
- 只是用來被其他多個構造方法調用,用於減少重復代碼。
關鍵字小結
本節我們提到了多個關鍵字,這裏匯總一下:
- public:可以修飾類、類方法、類變量、實例變量、實例方法、構造方法,表示可被外部訪問。
- private:可以修飾類、類方法、類變量、實例變量、實例方法、構造方法,表示不可以被外部訪問,只能在類內被使用。
- static: 修飾類變量和類方法,它也可以修飾內部類(後續章節介紹)。
- this:表示當前實例,可以用於調用其他構造方法,訪問實例變量,訪問實例方法。
- final: 修飾類變量、實例變量,表示只能被賦值一次,final也可以修飾實例方法和局部變量(後續章節介紹)。
類和對象的生命周期
類
在程序運行的時候,當第一次通過new創建一個類的對象的時候,或者直接通過類名訪問類變量和類方法的時候,Java會將類加載進內存,為這個類型分配一塊空間,這個空間會包括類的定義,它有哪些變量,哪些方法等,同時還有類的靜態變量,並對靜態變量賦初始值。後續文章會進一步介紹有關細節。
類加載進內存後,一般不會釋放,直到程序結束。一般情況下,類只會加載一次,所以靜態變量在內存中只有一份。
對象
當通過new創建一個對象的時候,對象產生,在內存中,會存儲這個對象的實例變量值,每new一次,對象就會產生一個,就會有一份獨立的實例變量。
每個對象除了保存實例變量的值外,可以理解還保存著對應類型即類的地址,這樣,通過對象能知道它的類,訪問到類的變量和方法代碼。
實例方法可以理解為一個靜態方法,只是多了一個參數this,通過對象調用方法,可以理解為就是調用這個靜態方法,並將對象作為參數傳給this。
對象的釋放是被Java用垃圾回收機制管理的,大部分情況下,我們不用太操心,當對象不再被使用的時候會被自動釋放。
具體來說,對象和數組一樣,有兩塊內存,保存地址的部分分配在棧中,而保存實際內容的部分分配在堆中。棧中的內存是自動管理的,函數調用入棧就會分配,而出棧就會釋放。
堆中的內存是被垃圾回收機制管理的,當沒有活躍變量指向對象的時候,對應的堆空間就可能被釋放,具體釋放時間是Java虛擬機自己決定的。活躍變量,具體的說,就是已加載的類的類變量,和棧中所有的變量。
小結
本 節我們主要從自定義數據類型的角度介紹了類,談了如何定義類,以及如何創建對象,如何使用類。自定義類型由類變量、類方法、實例變量和實例方法組成,為方 便對實例變量賦值,介紹了構造方法。本節引入了多個關鍵字,我們介紹了這些關鍵字的含義。最後我們介紹了類和對象的生命周期。
通過類實現自定義數據類型,封裝該類型的數據所具有的屬性和操作,隱藏實現細節,從而在更高的層次上(類和對象的層次,而非基本數據類型和函數的層次)考慮和操作數據,是計算機程序解決復雜問題的一種重要的思維方式。
本節介紹的Point類,其屬性只有基本數據類型,下節我們介紹類的組合,以表達更為復雜的概念。
【轉載】計算機程序的思維邏輯 (13) - 類