重新認識java(一) ---- 萬物皆對象

如果你現實中沒有對象,至少你在java世界裏會有茫茫多的對象,聽起來是不是很激動呢?

對象,引用,類與現實世界

現實世界裏有許許多多的生物,非生物,跑的跳的飛的,過去的現在的未來的,令人眼花繚亂。我們編程的目的,就是解決現實生活中的問題。所以不可避免的我們要和現實世界中各種奇怪的東西打交道。

在現實世界裏,你新認識了一個朋友,你知道他長什麼樣,知道了他的名字年齡,地址。知道他喜歡幹什麼有什麼特長。你想用java語言描述一下這個人,你應該怎麼做呢?

這個時候,就有了類的概念。每一個類對應現實世界中的某一事物。比如現實世界中有人。那麼我們就創建一個關於“人”的類。

每一個人都有名字,都有地址等等個人信息。那麼我們就在“人”的類裏面添加這些屬性。

每一個人都會吃,會走路,那麼我們就在“人”的類裏面添加吃和走的方法。

當這個世界又迎來了一個新生命,我們就可以“new”一個“人”,“new”出來的就叫”對象“。

每一個人一出生,父母就會給他取個名字。在程序裏,我們需要用一種方式來操作這個“對象”,於是,就出現了引用。我們通過引用來操作對象,設置對象的屬性,操作對象的方法。

這就是最基本的面向對象。

現實世界的事物】 —抽象—> 【 】—new—>【對象 】<—控制— 【引用

從創建一個對象開始

創建對象的前提是先得有一個類。我們先自己創建一個person類。

//Person類
public class Person {
    private String name;
    private int age;

    public void eat(){
        System.out.println("i am eating");
    }
}

創建一個person對象。

    Person p = new Person();

怎麼理解這句簡單的代碼呢?

  • new Person :一個Person類型的對象
  • () : 這個括號相當於調用了person的無參構造方法
  • p : Person對象的引用

有的人會認爲p就是new出來的Person對象。這是錯誤的理解,p只是一個Person對象的引用而已。那麼問題來了,什麼是引用?什麼又是對象呢?這個要從內存說起。

創建對象的過程

java大體上會把內存分爲四塊區域:堆,棧,靜態區,常量區。

  • 堆 : 位於RAM中,用於存放所有的java對象。
  • 棧 : 位於RAM中,引用就存在於棧中。
  • 靜態區 : 位於RAM中,被static修飾符修飾的變量會被放在這裏
  • 常量區 :位於ROM中, 很明顯,放常量的。

事實上,我們不需要關心java的對象,變量到底存在了哪裏,因爲jvm會幫我們處理好這些。但是理解了這些,有助於提高我們的水平。

當執行這句代碼的時候。

Person p = new Person();

首先,會在堆中開闢一塊空間存放這個新來的Person對象。然後,會創建一個引用p,存放在棧中,這個引用p指向Person對象(事實上是,p的值就是Person對象的內存地址)。

這樣,我們通過訪問p,然後得到了Person的內存地址,進而找到了Person對象。

然後又有了這樣一句代碼:

Person p2 = p;

這句代碼的含義是:
創建了一個新的引用,保存在棧中,引用的地址也指向Person的地址。這個時候,你通過p2來改變Person對象的狀態,也會改變p的結果。因爲它們指向同一個對象。(String除外,之後會專門講String)

此時,內存中是這樣的:

這裏寫圖片描述

用一種很通俗的方式來講解一下引用和對象。

大家都應該用過windows吧。win有一個神奇的東西叫做快捷方式。我們桌面的圖標大部分都是快捷方式。它並不是我們安裝在電腦上的應用的可執行文件(不是.exe文件),那麼爲什麼點擊它可以打開應用程序呢?這個我不用講了把。

我們的對象和引用就和快捷方式和它連接的文件一樣。

我們不直接對文件進行操作,而是通過快捷方式來進行操作。快捷方式不能獨立存在,同樣,引用也不能獨立存在(你可以只創建一個引用,但是當你要使用它的時候必須得給它賦值,否則它將毫無用處)。

一個文件可以有多個快捷方式,同樣一個對象也可以有多個引用。而一個引用只能同時對應一個對象。

在java裏,“=”不能被看成是一個賦值語句,它不是在把一個對象賦給另外一個對象,它的執行過程實質上是將右邊對象的地址傳給了左邊的引用,使得左邊的引用指向了右邊的對象。java表面上看起來沒有指針,但它的引用其實質就是一個指針。在java裏,“=”語句不應該被翻譯成賦值語句,因爲它所執行的確實不是一個簡單的賦值過程,而是一個傳地址的過程,被譯成賦值語句會造成很多誤解,譯得不準確。

特例:基本數據類型

爲什麼會有特例呢?因爲用new操作符創建的對象會存在堆裏,二在堆裏開闢空間等行爲效率較操作棧要低。而我們平時寫代碼的時候會經常創建一些“小變量”,比如int i = 1;如果每次都用Interger來new一個,效率不是很高而且浪費內存。

所以針對這些情況,java提供了“基本數據類型”,基本數據類型一共有八種,每一個基本數據類型存放在棧中,而他們的值存放在常量區中。

舉個例子:

int i = 2;
int j = 2;

我們需要知道的是,在常量區中,相同的常量只會存在一個。當執行第一句代碼時。先查找常量區中有沒有2,沒有,則開闢一個空間存放2,然後在棧中存入一個變量i,讓i指向2;

執行第二句的時候,查找發現2已經存在了,所以就不開闢新空間了。直接在棧中保存一個新變量j,讓j指向2;

當然,java堆每一個基本數據類型都提供了對應的包裝類。我們依舊可以用new操作符來創建我們想要的變量。

Integer i = new Integer(1);
Integer j = new Integer(1);

但是,用new操作符創建的對象是不同的,也就是說,此時,i和j指向不同的內存地址。因爲每次調用new操作符,都會在堆開闢新的空間。

當然,說到基本數據類型,不得不提一下java的經典設計。

先看一段代碼:

這裏寫圖片描述

爲什麼一個是true一個是false呢?

我就不講了,應該都知道吧。我就貼一個Integer的源碼(jdk1.8)吧。

這裏寫圖片描述

Integer 類的內部定義了一個內部類,緩存了從-128到127的所有數字,所以,你懂得。

又一個特例 :String

String是一個特殊的類,因爲它被final修飾符所修飾,是一個不可改變的類。當然,看過java源碼後你會發現,基本類型的各個包裝類也被final所修飾。這裏以String爲例。

我們來看這樣一個例子

這裏寫圖片描述

執行第一句 : 常量區開闢空間存放“abc”,s1存放在棧中指向“abc”

執行第二句,s2 也指向 “abc”,

執行第三句,因爲“abc”已經存在,所以直接指向它。

所以三個變量指向同一塊內存地址,結果都爲true。

當s1內容改變的時候。這個時候,常量區開闢新的空間存放“bcd”,s1指向“bcd”,而s2和s3指向“abc”所以只有s2和s3相等。

這種情況下,s1,s2,s3都是字符串常量,類似於基本數據類型。(如果執行的是s1 = “abc”,那麼結果會都是true)

我們再看一個例子:

這裏寫圖片描述

執行第一行代碼: 在堆裏分配空間存放String對象,在常量區開闢空間存放常量“abc”,String對象指向常量,s1指向該對象。

執行第二行代碼:s2指向上一步new出來的string對象。

執行第三行代碼: 在堆裏分配新的空間存放String對象,新對象指向常量“abc”,s3指向該對象。

到這裏,很明顯,s1和s2指向的是同一個對象

接着就很詭異了,我們讓s1 依舊= “abc”,但是結果s1和s2指向的地址不同了。

怎麼回事呢?這就是String類的特殊之處了,new出來的String不再是上面的字符串常量,而是字符串對象。

由於String類是不可改變的,所以String對象也是不可改變的,我們每次給String賦值都相當於執行了一次new String(),然後讓變量指向這個新對象,而不是在原來的對象上修改。

當然,java還提供了StringBuffer類,這個是可以在原對象上做修改的。如果你需要修改原對象,那麼請使用StringBuffer類。

值傳遞和引用傳遞的戰爭

java是值傳遞還是引用傳遞的呢?毫無疑問,java是值傳遞的。那麼什麼又叫值傳遞和引用傳遞呢?

我們先來看一個例子:

這裏寫圖片描述

這是一個很經典的例子,我們希望調用了swap函數以後,a和b的值可以互換,但是事實上並沒有。爲什麼會這樣呢?

這就是因爲java是值傳遞的。也就是說,我們在調用一個需要傳遞參數的函數時,傳遞給函數的參數並不是我們傳進去的參數本身,而是它的副本。說起來比較拗口,但是其實原理很簡單。我們可以這樣理解:

一個有形參的函數,當別的函數調用它的時候,必須要傳遞數據。
比如swap函數,別的函數要調用swap就必須傳兩個整數過來。

這個時候,有一個函數按耐不住寂寞,扔了兩個整數過來,但是,swap函數有潔癖,它不喜歡用別人的東西,於是它把傳過來的參數複製了一份,然後對複製的數據修修改改,而別人傳過來的參數動根本沒動。

所以,當swap函數執行完畢之後,交換了的數據只是swap自己複製的那一份,而原來的數據沒變。

也可以理解爲別的函數把數據傳遞給了swap函數的形參,最後改變的只是形參而實參沒變,所以不會起到任何效果。

我們再來看一個複雜一點的例子(Person類添加了get,set方法):

這裏寫圖片描述

可以看到,我們把p1傳進去,它並沒有被替換成新的對象。因爲change函數操作的不是p1這個引用本身,而是這個引用的一個副本。

你依然可以理解爲,主函數將p1複製了一份然後變成了chagne函數的形參,最終指向新Person對象的是那個副本引用,而實參p1並沒有改變。

再來看一個例子:

這裏寫圖片描述

這次爲什麼就改變了呢?分析一下。

首先,new了一個Person對象,暫且叫他小明吧。然後p1指向小明。

小明10歲了,隨着時間的推移,小明的年齡要變了,調用了一下changgeAge方法,把小明的引用傳了進去。

傳遞的過程中,changgeAge也有潔癖,於是複製了一份小明的引用,這個副本也指向小明。

然後changgeAge通過自己的副本引用,改變了小明的年齡。

由於是小明這個對象被改變了,所以所有小明的引用調用方法得到的年齡都會改變

所以就變了。

最後簡單的總結一下。

java的傳值過程,其實傳的是副本,不管是變量還是引用。所以,不要期待把變量傳遞給一個函數來改變變量本身。

對象的強引用,軟引用,弱引用和虛引用

Java中是JVM負責內存的分配和回收,這樣雖然使用方便,程序不用再像使用c那樣操心內存,但同時也是它的缺點(不夠靈活)。爲了解決內存操作不靈活這個問題,可以採用軟引用等方法。

先介紹一下這四種引用:

  • 強引用

    以前我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似於必不可少的生活用品,垃圾回收器絕不會回收它。當內存空 間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。

  • 軟引用(SoftReference)

    如果一個對象只具有軟引用,那就類似於可有可物的生活用品。如果內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。

    軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收,JAVA虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。

  • 弱引用(WeakReference)

    如果一個對象只具有弱引用,那就類似於可有可物的生活用品。弱引用與軟引用的區別在於:只具有弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它 所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由於垃圾回收器是一個優先級很低的線程, 因此不一定會很快發現那些只具有弱引用的對象。

    弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。

  • 虛引用(PhantomReference)

    “虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定對象的生命週期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。

    虛引用主要用來跟蹤對象被垃圾回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃 圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是 否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序如果發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的內存被回收之前採取必要的行動。

在實際開發中,弱引用和虛引用不常用,用得比較多的是軟引用,因爲它可以加速jvm的回收。

軟引用的使用方式:

這裏寫圖片描述

關於軟引用,我之後會單獨寫一篇文章,所以這裏先一筆帶過。

對象的複製

java除了用new來創建對象,還可以通過clone來複制對象。

那麼這兩種方式有什麼相同和不同呢?

  • new

new操作符的本意是分配內存。程序執行到new操作符時,首先去看new操作符後面的類型,因爲知道了類型,才能知道要分配多大的內存空間。分配完內存之後,再調用構造函數,填充對象的各個域,這一步叫做對象的初始化,構造方法返回後,一個對象創建完畢,可以把他的引用(地址)發佈到外部,在外部就可以使用這個引用操縱這個對象。


  • clone

clone在第一步是和new相似的, 都是分配內存,調用clone方法時,分配的內存和源對象(即調用clone方法的對象)相同,然後再使用原對象中對應的各個域,填充新對象的域, 填充完成之後,clone方法返回,一個新的相同的對象被創建,同樣可以把這個新對象的引用發佈到外部。
如何利用clone的方式來得到一個對象呢?

看代碼:

這裏寫圖片描述

對Person類做了一些修改

看實現代碼:

這裏寫圖片描述

這樣就得到了一個和原來一樣的新對象。

深複製和淺複製

但是,細心並且善於思考的人可能一經發現了一個問題。

age是一個基本數據類型,支架clone沒什麼問題,但是name可是一個String類型的啊。我們clone後的對象裏的name和原來對象的name是不是指向同一個字符串常量呢?

做個試驗:

這裏寫圖片描述

果然,是同一個對象。如果你不能理解,那麼看這個圖。

這裏寫圖片描述

其實如果只是String還好,因爲String的不可變性,當你隨便修改一個值的時候,他們就會指向不同的地址了,但是除了String,其他都是可變的。這就危險了。

上面的這種情況,就是淺克隆。這種方式在你的屬性列表中有其他對象的引用的時候其實是很危險的。所以,我們需要深克隆。也就是說我們需要將這個對象裏的對象也clone一份。怎麼做呢?

在內存中通過字節流的拷貝是比較容易實現的。把母對象寫入到一個字節流中,再從字節流中將其讀出來,這樣就可以創建一個新的對象了,並且該新對象與母對象之間並不存在引用共享的問題,真正實現對象的深拷貝。

//使用該工具類的對象必須要實現 Serializable 接口,否則是沒有辦法實現克隆的。
public class CloneUtils {

    public static <T extends Serializable> T clone(T   obj){
        T cloneObj = null;
        try {
            //寫入字節流
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ObjectOutputStream obs = new   ObjectOutputStream(out);
            obs.writeObject(obj);
            obs.close();

            //分配內存,寫入原始對象,生成新對象
            ByteArrayInputStream ios = new  ByteArrayInputStream(out.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(ios);
            //返回生成的新對象
            cloneObj = (T) ois.readObject();
            ois.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cloneObj;
    }
}

使用該工具類的對象只要實現 Serializable 接口就可實現對象的克隆,無須繼承 Cloneable 接口實現 clone() 方法。

測試一下:

這裏寫圖片描述

很完美

這個時候,Person類實現了Serializable接口

是否使用複製,深複製還是淺複製看情況來使用。

關於序列化與反序列化以後會講。


這篇文章到這裏就暫時告一段落了,後續有補充的話我會繼續補充,有錯誤的話,我也會及時改正。歡迎大家提出問題。

博客同步更新在http://blog.improvecfan.cn
事例代碼放在github:https://github.com/CleverFan/JavaImprove

發佈了46 篇原創文章 · 獲贊 675 · 訪問量 75萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章