[JAVA] 只知物件屬性,不知類屬性?就算類答應,static都不答應

老夫不正經發表於2020-03-23

由淺入深——Java 類、物件、static成員

物件

在物件導向的思想中,一切事物都可以認為是物件——萬物皆物件,把物件定義成包含狀態和行為的一個實體,存在於現實世界中並且可以與其他實體區分開來的。物件具有狀態和行為;比如:想想你心儀的小姐姐,可以把這個小姐姐看作是一個物件,那麼該物件有兩方面的定義:狀態和行為;狀態,如身高,年齡,三圍,頭髮(長髮或者短髮)等;行為,如調戲你、跳舞,玩手機等。

通過多個相同型別的物件的狀態和行為分析,可以把物件抽象成類(class);我們把具有相同特性(狀態)和行為(功能)的物件的抽象定義類,物件的抽象是類,類例項化後便是物件,類的例項是物件,類其實就是物件的資料型別,但其和基本資料型別的差異在於類是程式設計師為了解決某些問題而自定義的,基本資料型別是計算機中的資料儲存單元。

Java 物件

在Java中,物件的狀態,用成員變數來描述;物件的行為,用方法來描述;故Java中類可以這樣定義,語法如下:

Java 類的語法

類定義示例程式碼:

類定義示例程式碼

定義Java 類時有一些必要的規範需要遵守:

  1. 類名一律使用英文或者國際通用的拼音符號,做到見名知義,如taobao,weixin,雖然是拼音,但卻是國際通用的,可以使用;
  2. 如果類使用了public修飾符,必須保證當前java檔名稱和當前類名相同,而且在一個java檔案中,只能有一個public修飾的類(class);
  3. 類名首字母大寫,如果類名是多個單片語成的,使用駝峰命名法,如: OperatingSystem(作業系統);

物件比較操作

先考慮下面的程式碼:

物件比較示例程式碼

上述示例程式碼執行結果為:

物件比較結果

為什麼會出現這樣的結果呢?都是同樣的值,為什麼會有不同的比較結果?那是因為**==!=這兩個比較運算子在比較基本資料型別和物件物件型別時是由區別的:**

  • 對於基本資料型別來說,比較的是值,也就是變數儲存的資料內容;
  • 對於引用資料型別來說,比較的是物件的引用,也就是其在堆記憶體中的地址值,每次使用new關鍵字建立物件,都會在堆中新開闢一塊記憶體空間儲存新建立的物件, 並且會為該記憶體空間生成一個唯一的地址,故記憶體空間不同,記憶體空間的地址值也就不同。

那麼哪些資料型別時基本資料型別,哪些是引用資料型別呢?

  • 基本資料型別:byte、short、char、int、long、float、double、boolean
  • 引用資料型別:除基本資料型別以外的所有資料型別都是引用資料型別,包括String和基本資料型別的封裝型別;

所以,如果要對物件的值做比較,就必須要是用物件的**equals()**方法了;這裡需要注意,**equals()方法並不適用於基本資料型別,對於基本資料型別的變數來說,使用== 和 !=**足夠了。下面用一個例子來實踐,程式碼如下:

上述案例輸出結果為:

由此可看出,使用物件的equals()方法是能正確比較物件的值的,因為Integer已經自定義了equals方法了,下面是原始碼:

不難發現,Integer的equals()方法的底層是使用基本資料型別的值做==比較的。如果是我們自定義的類,而且沒有重新定義equals()方法呢,結果又會是怎樣的,一起來看看:

輸出結果為:false。

因為在Java中,有一個所有引用型別都直接或者間接繼承的父類,Object;因此,也可以說在java中,所有類都是Object的子類,那麼,如果我們沒重新實現**equals()**方法,會預設呼叫Object的equals()方法,Object的equals()方法比較的是物件的引用,所以結果輸出為false。

所以想要使用自定義物件的equals方法比較物件的值,那麼就必須重新實現equals方法。

物件的列印操作

預設情況下,Java物件列印的效果是:類的名稱@十六進位制的hashCode,比如:

輸出為:com.strlite.admin.demo.Value@79b4d0f,com.strlite.admin.demo.Value是類的名稱,79b4d0f是一個十六進位制的數,是物件在堆中的記憶體地址。

重寫toString() 方法

可以通過重寫toString() 方法來改變物件的列印效果:

輸出為:

  • i = 13

物件的生命週期

物件的開始:每次使用new關鍵字建立物件,就會在記憶體中開闢新的空間儲存物件資訊,此時物件開始存在。

物件的結束:當堆中的物件,沒有被任何變數所引用,此時該物件就成了垃圾,等待垃圾回收器(GC)來回收;當物件被回收後,物件被銷燬,物件佔用的記憶體空間被釋放,物件的生命週期結束。

匿名物件

物件建立之後沒有將其賦給某一個變數。匿名物件只是在堆中開闢一塊新的記憶體空間,但是沒有把該空間地址賦給任何變數。因為沒有變數引用指向,所以匿名物件僅僅只能使用一次,一般會把匿名物件作為方法的引數傳遞。

  • new Integer(); // 建立的就是匿名物件

構造器

  • Integer i = new Integer();

在建立物件時使用的特殊方法,出現new 關鍵字之後的方法,稱之為構造方法、構造器、建構函式(Constructor)

構造器的作用

  1. 用於建立物件,但是必須和 new 一起使用;比如:new Integer(13);
  2. 完成物件的初始化操作,可以建立帶引數的構造器,為成員變數賦初始值;

構造器的特點

  1. 構造器的名稱和當前所在類的名稱相同;
  2. 構造器是一個特殊的方法,其沒有定義返回型別,所有不必使用void作為返回型別。 假設需要寫返回型別,也應該這樣寫:Integer Integer(); 但沒有這樣的必要;
  3. 在構造器中,不需要使用return語句,其實構造器是有返回值的,會預設返回當前建立物件的引用。

如果類中沒有構造器,編譯器會自動建立一個預設的無參構造器

我們將上述程式碼經過編譯,得到位元組碼檔案,再將位元組碼檔案反編譯,反編譯的結果如下:

通過反編譯後的結果,不難發現,即便我們沒有建立構造器,編譯器也會為我們建立一個預設的,編譯器建立的預設構造器有以下的特點:

  • 符合構造器特點;
  • 無引數的;
  • 無方法體;
  • 如果類沒有使用public修飾, 則編譯器問起建立的構造器也沒有public修飾;使用了public修飾,則編譯器建立的構造器也使用public修飾;

如果類中沒有構造器,編譯器會自動建立一個預設的無參構造器。但是,如果我們顯式地定義了一個構造器,則編譯器不再建立預設構造器。案例如下所示:

通過上述對比,不難發現,當類中存在一個構造器時,編譯器便不會建立預設的構造器,而是使用我們定義的構造器,由此可得出:在一個類中,至少存在一個構造器

static 修飾符

假如每個人都有name和age兩個狀態,但是不同人的name和age是不一樣的;也就說name和age是屬於物件的。但是在生活中有些東西並不是單單屬於某一個物件的,而是屬於整個類的,比如:每個人都會老去、都會死。

所以,狀態和行為的所屬也應該有物件和類之分。 有的狀態和行為應該屬於物件,不同的物件,狀態和行為可以不一樣;而有的狀態和行為應該屬於類,不屬於物件。為了區別與物件的狀態和行為,引入static修飾符來修飾類的狀態和行為。

static修飾符表示靜態的,可修飾欄位、方法、內部類,其修飾的成員屬於類,static修飾的資源屬於類級別,區別於物件級別。static的真正作用是用來區別欄位、方法、內部類、初始化程式碼塊是屬於物件還是屬於類本身。

static修飾符的特點

  1. static修飾的成員(欄位/方法),隨著所在類的載入而載入,當JVM把位元組碼載入進JVM的時候,static修飾的成員已經在記憶體中存在了。
  2. 優先於物件的存在,物件是我們手動通過new關鍵字建立出來的,static成員是JVM建立的;
  3. satic修飾的成員被該型別的所有物件所共享,該類建立的任何物件都可以訪問該類的static成員;
  4. 直接使用類名訪問static成員因為static修飾的成員直接屬於類,不屬於物件,所以可以直接使用類名訪問static成員.

下面我們通過一個案例來實踐static關鍵字的使用:

static修改的變數稱為常量,會長時間存在於JVM記憶體中,所以JVM也會為它分配一定的儲存空間,以下便是static常量在jvm 中的記憶體模型:

JVM會將靜態變數儲存在方法區中,以便於及時呼叫;並保證其能夠長時間儲存於JVM中。

類成員和例項成員的訪問

類中的成員:欄位,方法,內部類。

  • 類成員:使用static修飾的成員,直接屬於類,通過類名.static成員來訪問;
  • 例項成員:沒有使用static修飾的成員,例項成員只屬於物件, 通過物件來訪問非static欄位和非static方法;

一般情況下,類成員只能訪問類成員,例項成員只能訪問例項成員;但深究發現,物件其實可以訪問類成員,但是底層依然使用類名訪問的。

static方法

static方法中,只能呼叫static成員;非static方法,可以訪問靜態成員,也可以訪問例項成員;

那什麼時候定義成static的欄位和方法:

  • 如果這個一個狀態/行為屬於整個事物(類),被所有物件所共享,就直接使用static修飾;
  • 在開發中,往往把工具方法使用static修飾,比如:陣列中常用的java.util.Arrays中的方法;

如果不使用static修飾,則這些方法屬於該類的物件,我們得先建立物件才能呼叫方法,在開發中工具物件只需要一份即可,可能建立N個物件,此時可以考慮使用單例設計模式。

類成員的使用

好處:對物件的共享資料進行單獨空間的儲存,節省空間,沒有必要每一個物件中都儲存一份,可以直接被類名呼叫。

弊端:生命週期過長。

區域性變數初始化

區域性變數定義後,必須顯式初始化後才能使用,因為JVM不會為區域性變數執行初始化操作。這就意味著,定義區域性變數後,JVM並未為這個變數分配記憶體空間。直到程式為這個變數賦值時,系統才會為區域性變數分配記憶體,並將初始值儲存到該記憶體中。

區域性變數不屬於任何類或例項,因此它是儲存在其所在方法的棧幀記憶體中。

  • 基本資料區域性變數:基本資料型別變數的值會直接儲存到該變數所對應的記憶體中。
  • 引用資料區域性變數:變數記憶體中存的是堆中物件的地址,通過該地址引用到該變數實際指向的堆裡的物件。

棧幀記憶體中的變數隨方法或程式碼塊的執行結束而銷燬,無須JVM回收。

一點小建議

  • 開發中應該儘量縮小變數的作用範圍,如此在記憶體中停留時間越短,效能也就更高。
  • 合理使用static修飾,一般只有定義工具方法的時候使用;
  • static方法需要訪問的變數,只有該變數確實屬於類,才使用static修飾欄位;
  • 儘量使用區域性變數;

完結。老夫雖不正經,但老夫一身的才華

相關文章