1. 程式人生 > >重新認識java(一) ---- 萬物皆物件

重新認識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介面

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

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

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

相關推薦

重新認識java ---- 萬物物件

如果你現實中沒有物件,至少你在java世界裡會有茫茫多的物件,聽起來是不是很激動呢? 物件,引用,類與現實世界 現實世界裡有許許多多的生物,非生物,跑的跳的飛的,過去的現在的未來的,令人眼花繚亂。我們程式設計的目的,就是解決現實生活中的問題。所以

重新認識java ---- Enum列舉類

有的人說,不推薦使用列舉。有的人說,列舉很好用。究竟怎麼使用,如何使用,仁者見仁智者見智。總之,先學會再說~ 為什麼要引入列舉類 一個小案例 你寫了一個小程式,不過好久不用了,突然有一天,你想使用一下它。程式要想正確執行,需要將今天星期幾存

重新認識java ---- 面向物件之繼承!

學習一個新知識的第一步,就是要知道它是什麼,然後要知道為什麼要用它,最後要知道如何使用它。這篇文章,我們重新認識一下java中的繼承。 繼承是個什麼東西 我們先來看一下上一篇文章中的程式碼: 你會發現,這兩個類中都有name屬性,都有

重新認識java --- 不積跬步無以至千里

好高騖遠,眼高手低,是你前進路上最大的絆腳石 — 致走在學習道路上的人 p.s. 本篇文章沒有技術含量。 關於自己 先說說我自己吧。目前是一名軟體工程的大三學生。前幾天一直在迷茫:究竟是技多不壓身還是貪多嚼不爛。 大一的一年,我沒有好好學

重新認識java ---- java中的另類:static關鍵字附程式碼塊知識

你知道麼,static的用法至少有五種? 初識static static是“靜態”的意思,這個大家應該都清楚,靜態變數,靜態方法大家也都能隨口道來。但是,你真的理解靜態變數和靜態方法麼?除了這些static還有什麼用處? 事實上,static大

重新認識java ---- 內部類

注意注意!!!前排提示!!!本篇文章過長,最好收藏下來慢慢看,如果你之前對內部類不是很熟悉,一次性看完,大概你會懵逼。。。 1. 內部類概述 一個類的定義放在另一個類的內部,這個類就叫做內部類。內部類是一種非常有用的特性,因為它允許你把一些邏輯相

Java

void ring 獲取 .get tom 一個 創建 print etag public class Puppy{ int puppyAge; public Puppy(String name){ // 這個構造器僅有一個參數:name

認識 Mysql

支持 名稱 描述 table 體積 廣泛 ike 表名 com 1、什麽是數據 官方表達:數據(data)是事實或觀察的結果,是對客觀事物的邏輯歸納,是用於表示客觀事物的未經加工的的原始素材。       數據是信息的表現形式和載體,可以是符號、文字、數

GUI庫之認識Tkinter

技術分享 設計 自身 src idle pre 標題欄 scl png 一、介紹 Tkinter是Python默認的GUI庫,我們經常使用的IDLE就是用Tkinter設計出來的,因此我們在使用的時候直接導入Tkinter模塊就好了。 1.特點:可移植性、靈活性高 2.構成

Core JavaJava程式設計概述

Java “白皮書”的關鍵術語 簡單性 面向物件 分散式 健壯性 安全性 體系結構中立 可移植性 解釋型 高效能 多執行緒 動態性 簡單性 Java語法是C++語法的一個“純淨”版本。這裡沒有標頭檔案、

Elasticsearch學習系列 Linux服務部署--Java

 Elasticsearch   Elasticsearch(以下簡稱ES)是一款Java語言開發的基於Lucene的高效全文搜尋引擎。它提供了一個分散式多使用者能力的基於RESTful web介面的全文搜尋和分析服務,並作為Apache許可條款下的開放原始碼釋出,是當前流行的企業級搜尋引擎。設計

Java之前世今生

一、Java語言是什麼? 一種計算機程式語言,名字取自咖啡。 二、Java語言發展簡史 Java語言之父 : James Gosling SUN (Stanford University Network 斯坦福大學網路公司) 1995年5月23日 Java

Java架構-JavaSE之類與物件

閱讀目錄(Content) 一、OOP概述 二、類與物件和物件與引用的關係   2.1、類與物件的關係 三、方法的定義和呼叫   3.1、方法的定義   3.2、方法呼叫 四、呼叫方法時的傳參 五、this關鍵字 六、建立與初始化物件 七、構造器 終於到了要學習面向物件程式設

java 反射 獲取Class物件的三種方式

package com.reflect; /** * 三種獲得Class物件的方式 * @author lr * */ public class Demo1 { public static void main(String[] args) throws ClassNotFound

效能測試—認識JMeter

 效能測試—認識JMeter(一) 《零成本web效能測試》第二章 JMeter基礎知識總結和自己的理解   一、JMeter百度詞條概念  Apache JMeter是Apache組織開發的基於Java的壓力測試工具。用於對軟體做壓力測試,它最初被設計用於Web應用測試,但後來擴充

黑馬程式設計師——Java面向物件之匿名物件、程式碼塊、static關鍵字等

   a)子類只繼承父類的預設(預設)建構函式,即無形參建構函式。如果父類沒有預設建構函式,那子類不能從父類繼承預設建構函式。    b)子類從父類處繼承來的父類預設建構函式,不能成為子類的預設建構函式。    c)在建立物件時,先呼叫父類預設建構函式對物件進行初始化,然後呼叫子類自身自己定義的建構函

面試題--Java

1.Springmvc獨有的5個註解 1)@Controller @Controller 用於標記在一個類上,使用它標記的類就是一個SpringMVC Controller 物件。分發處理器將會掃描使用了該註解的類的方法。通俗來說,被Contr

重新學習java ---- 組合、聚合與繼承的愛恨情仇

有人學了繼承,認為他是面向物件特點之一,就在所有能用到繼承的地方使用繼承,而不考慮究竟該不該使用,無疑,這是錯誤的。那麼,究竟該如何使用繼承呢?java中類與類之間的關係大部分的初學者只知道java中兩個類之間可以是繼承與被繼承的關係,可是事實上,類之間的關係大體上存在五

一頭撞進Java:Java環境配置與你的第一行程式碼

0、引子 相信很多人為了學Java都會在網上找各種各樣的Java電子書,如Deitel出版社的Java:How to Program(Java大學教程),國內的《瘋狂Java講義》,或者是某些網站的線

數據結構java數組鏈表

tro 自己 輸出 學生 運算 rri 判空 row 運算符 鏈表是數據結構中最基礎的內容,鏈表在存儲結構上分成兩種:數組形式儲存,鏈式存儲。 相比c語言需要的結構體,在java中由於有了面向對象編程,將指針‘藏’了起來,不需要分配內存。 所以只需要創建一個對象數組,為了能