Java編程的邏輯 (13) - 類

本系列文章經補充和完善,已修訂整理成書《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)

ab次方

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,它至少作了兩件事:

  1. 分配內存,以存儲新對象的數據,對象數據包括這個對象的屬性,具體包括其實例變量x和y。
  2. 給實例變量設置默認值,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類,其屬性只有基本數據類型,下節咱們介紹類的組合,以表達更爲複雜的概念。

----------------

未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),從入門到高級,深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。原創文章,保留全部版權。

-----------

更多相關原創文章

計算機程序的思惟邏輯 (14) - 類的組合

計算機程序的思惟邏輯 (15) - 初識繼承和多態

計算機程序的思惟邏輯 (16) - 繼承的細節

計算機程序的思惟邏輯 (17) - 繼承實現的基本原理

計算機程序的思惟邏輯 (18) - 爲何說繼承是把雙刃劍

計算機程序的思惟邏輯 (19) - 接口的本質

計算機程序的思惟邏輯 (20) - 爲何要有抽象類?

計算機程序的思惟邏輯 (21) - 內部類的本質

相關文章
相關標籤/搜索