本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》,由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買,京東自營連接:http://item.jd.com/12299018.htmlhtml
類
編程
上節咱們介紹了函數調用的基本原理,本節和接下來幾節,咱們探索類的世界。swift
程序主要就是數據以及對數據的操做,爲方便理解和操做,高級語言使用數據類型這個概念,不一樣的數據類型有不一樣的特徵和操做,Java定義了八種基本數據類型,其中,四種整形byte/short/int/long,兩種浮點類型float/double,一種真假類型boolean,一種字符類型char,其餘類型的數據都用類這個概念表達。數組
前兩節咱們暫時將類看作函數的容器,在某些狀況下,類也確實基本上只是函數的容器,但類更多表示的是自定義數據類型,咱們先從容器的角度,而後從自定義數據類型的角度談談類。微信
函數容器
dom
咱們看個例子,Java API中的類Math,它裏面主要就包含了若干數學函數,下表列出了其中一些:ide
Math函數函數 |
功能post |
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 { 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; }
這兩個就是構造方法,構造方法能夠有多個。不一樣於通常方法,構造方法有一些特殊的地方:
與普通方法同樣,構造方法也能夠重載。第二個構造方法是比較容易理解的,使用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編程及計算機技術的本質。原創文章,保留全部版權。
-----------
更多相關原創文章