如果你現(xiàn)實(shí)中沒有對象精绎,至少你在java世界里會(huì)有茫茫多的對象澈吨,聽起來是不是很激動(dòng)呢更胖?
對象艘狭,引用唾那,類與現(xiàn)實(shí)世界
現(xiàn)實(shí)世界里有許許多多的生物霜医,非生物线衫,跑的跳的飛的啡氢,過去的現(xiàn)在的未來的菩暗,令人眼花繚亂掰曾。我們編程的目的,就是解決現(xiàn)實(shí)生活中的問題停团。所以不可避免的我們要和現(xiàn)實(shí)世界中各種奇怪的東西打交道旷坦。
在現(xiàn)實(shí)世界里掏熬,你新認(rèn)識了一個(gè)朋友,你知道他長什么樣秒梅,知道了他的名字年齡旗芬,地址。知道他喜歡干什么有什么特長捆蜀。你想用java語言描述一下這個(gè)人疮丛,你應(yīng)該怎么做呢?
這個(gè)時(shí)候辆它,就有了類的概念誊薄。每一個(gè)類對應(yīng)現(xiàn)實(shí)世界中的某一事物。比如現(xiàn)實(shí)世界中有人锰茉。那么我們就創(chuàng)建一個(gè)關(guān)于“人”的類暇屋。
每一個(gè)人都有名字,都有地址等等個(gè)人信息洞辣。那么我們就在“人”的類里面添加這些屬性咐刨。
每一個(gè)人都會(huì)吃,會(huì)走路扬霜,那么我們就在“人”的類里面添加吃和走的方法定鸟。
當(dāng)這個(gè)世界又迎來了一個(gè)新生命,我們就可以“new”一個(gè)“人”著瓶,“new”出來的就叫”對象“联予。
每一個(gè)人一出生,父母就會(huì)給他取個(gè)名字材原。在程序里沸久,我們需要用一種方式來操作這個(gè)“對象”,于是余蟹,就出現(xiàn)了引用卷胯。我們通過引用來操作對象,設(shè)置對象的屬性威酒,操作對象的方法窑睁。
這就是最基本的面向?qū)ο蟆?/p>
【 現(xiàn)實(shí)世界的事物】 ---抽象---> 【類 】---new--->【對象 】<---控制--- 【引用】
從創(chuàng)建一個(gè)對象開始
創(chuàng)建對象的前提是先得有一個(gè)類。我們先自己創(chuàng)建一個(gè)person類葵孤。
//Person類
public class Person {
private String name;
private int age;
public void eat(){
System.out.println("i am eating");
}
}
創(chuàng)建一個(gè)person對象担钮。
Person p = new Person();
怎么理解這句簡單的代碼呢?
- new Person :一個(gè)Person類型的對象
- () : 這個(gè)括號相當(dāng)于調(diào)用了person的無參構(gòu)造方法
- p : Person對象的引用
有的人會(huì)認(rèn)為p就是new出來的Person對象尤仍。這是錯(cuò)誤的理解箫津,p只是一個(gè)Person對象的引用而已。那么問題來了,什么是引用苏遥?什么又是對象呢送挑?這個(gè)要從內(nèi)存說起。
創(chuàng)建對象的過程
java大體上會(huì)把內(nèi)存分為四塊區(qū)域:堆暖眼,棧惕耕,靜態(tài)區(qū),常量區(qū)诫肠。
- 堆 : 位于RAM中司澎,用于存放所有的java對象。
- 棧 : 位于RAM中栋豫,引用就存在于棧中挤安。
- 靜態(tài)區(qū) : 位于RAM中,被static修飾符修飾的變量會(huì)被放在這里
- 常量區(qū) :位于ROM中丧鸯, 很明顯蛤铜,放常量的。
事實(shí)上丛肢,我們不需要關(guān)心java的對象围肥,變量到底存在了哪里,因?yàn)閖vm會(huì)幫我們處理好這些蜂怎。但是理解了這些穆刻,有助于提高我們的水平。
當(dāng)執(zhí)行這句代碼的時(shí)候杠步。
Person p = new Person();
首先氢伟,會(huì)在堆中開辟一塊空間存放這個(gè)新來的Person對象。然后幽歼,會(huì)創(chuàng)建一個(gè)引用p朵锣,存放在棧中,這個(gè)引用p指向Person對象(事實(shí)上是甸私,p的值就是Person對象的內(nèi)存地址)诚些。
這樣,我們通過訪問p颠蕴,然后得到了Person的內(nèi)存地址泣刹,進(jìn)而找到了Person對象。
然后又有了這樣一句代碼:
Person p2 = p;
這句代碼的含義是:
創(chuàng)建了一個(gè)新的引用犀被,保存在棧中,引用的地址也指向Person的地址外冀。這個(gè)時(shí)候寡键,你通過p2來改變Person對象的狀態(tài),也會(huì)改變p的結(jié)果雪隧。因?yàn)樗鼈冎赶蛲粋€(gè)對象西轩。(String除外员舵,之后會(huì)專門講String)
此時(shí),內(nèi)存中是這樣的:
用一種很通俗的方式來講解一下引用和對象藕畔。
大家都應(yīng)該用過windows吧马僻。win有一個(gè)神奇的東西叫做快捷方式。我們桌面的圖標(biāo)大部分都是快捷方式注服。它并不是我們安裝在電腦上的應(yīng)用的可執(zhí)行文件(不是.exe文件)韭邓,那么為什么點(diǎn)擊它可以打開應(yīng)用程序呢?這個(gè)我不用講了把溶弟。
我們的對象和引用就和快捷方式和它連接的文件一樣女淑。
我們不直接對文件進(jìn)行操作,而是通過快捷方式來進(jìn)行操作辜御⊙寄悖快捷方式不能獨(dú)立存在,同樣擒权,引用也不能獨(dú)立存在(你可以只創(chuàng)建一個(gè)引用袱巨,但是當(dāng)你要使用它的時(shí)候必須得給它賦值,否則它將毫無用處)碳抄。
一個(gè)文件可以有多個(gè)快捷方式瓣窄,同樣一個(gè)對象也可以有多個(gè)引用。而一個(gè)引用只能同時(shí)對應(yīng)一個(gè)對象纳鼎。
在java里俺夕,“=”不能被看成是一個(gè)賦值語句,它不是在把一個(gè)對象賦給另外一個(gè)對象贱鄙,它的執(zhí)行過程實(shí)質(zhì)上是將右邊對象的地址傳給了左邊的引用劝贸,使得左邊的引用指向了右邊的對象。java表面上看起來沒有指針逗宁,但它的引用其實(shí)質(zhì)就是一個(gè)指針映九。在java里,“=”語句不應(yīng)該被翻譯成賦值語句瞎颗,因?yàn)樗鶊?zhí)行的確實(shí)不是一個(gè)簡單的賦值過程件甥,而是一個(gè)傳地址的過程,被譯成賦值語句會(huì)造成很多誤解哼拔,譯得不準(zhǔn)確引有。
特例:基本數(shù)據(jù)類型
為什么會(huì)有特例呢?因?yàn)橛胣ew操作符創(chuàng)建的對象會(huì)存在堆里倦逐,二在堆里開辟空間等行為效率較操作棧要低譬正。而我們平時(shí)寫代碼的時(shí)候會(huì)經(jīng)常創(chuàng)建一些“小變量”,比如int i = 1;如果每次都用Interger來new一個(gè)曾我,效率不是很高而且浪費(fèi)內(nèi)存粉怕。
所以針對這些情況,java提供了“基本數(shù)據(jù)類型”抒巢,基本數(shù)據(jù)類型一共有八種贫贝,每一個(gè)基本數(shù)據(jù)類型存放在棧中,而他們的值存放在常量區(qū)中蛉谜。
舉個(gè)例子:
int i = 2;
int j = 2;
我們需要知道的是稚晚,在常量區(qū)中,相同的常量只會(huì)存在一個(gè)悦陋。當(dāng)執(zhí)行第一句代碼時(shí)蜈彼。先查找常量區(qū)中有沒有2,沒有俺驶,則開辟一個(gè)空間存放2幸逆,然后在棧中存入一個(gè)變量i,讓i指向2暮现;
執(zhí)行第二句的時(shí)候还绘,查找發(fā)現(xiàn)2已經(jīng)存在了,所以就不開辟新空間了栖袋。直接在棧中保存一個(gè)新變量j拍顷,讓j指向2;
當(dāng)然塘幅,java堆每一個(gè)基本數(shù)據(jù)類型都提供了對應(yīng)的包裝類昔案。我們依舊可以用new操作符來創(chuàng)建我們想要的變量。
Integer i = new Integer(1);
Integer j = new Integer(1);
但是电媳,用new操作符創(chuàng)建的對象是不同的踏揣,也就是說,此時(shí)匾乓,i和j指向不同的內(nèi)存地址捞稿。因?yàn)槊看握{(diào)用new操作符,都會(huì)在堆開辟新的空間拼缝。
當(dāng)然娱局,說到基本數(shù)據(jù)類型,不得不提一下java的經(jīng)典設(shè)計(jì)咧七。
先看一段代碼:
為什么一個(gè)是true一個(gè)是false呢衰齐?
我就不講了,應(yīng)該都知道吧猪叙。我就貼一個(gè)Integer的源碼(jdk1.8)吧娇斩。
Integer 類的內(nèi)部定義了一個(gè)內(nèi)部類仁卷,緩存了從-128到127的所有數(shù)字穴翩,所以犬第,你懂得。
又一個(gè)特例 :String
String是一個(gè)特殊的類芒帕,因?yàn)樗籪inal修飾符所修飾歉嗓,是一個(gè)不可改變的類。當(dāng)然背蟆,看過java源碼后你會(huì)發(fā)現(xiàn)鉴分,基本類型的各個(gè)包裝類也被final所修飾。這里以String為例带膀。
我們來看這樣一個(gè)例子
執(zhí)行第一句 : 常量區(qū)開辟空間存放“abc”志珍,s1存放在棧中指向“abc”
執(zhí)行第二句,s2 也指向 “abc”垛叨,
執(zhí)行第三句伦糯,因?yàn)椤癮bc”已經(jīng)存在,所以直接指向它嗽元。
所以三個(gè)變量指向同一塊內(nèi)存地址敛纲,結(jié)果都為true。
當(dāng)s1內(nèi)容改變的時(shí)候剂癌。這個(gè)時(shí)候淤翔,常量區(qū)開辟新的空間存放“bcd”,s1指向“bcd”佩谷,而s2和s3指向“abc”所以只有s2和s3相等旁壮。
這種情況下,s1,s2,s3都是字符串常量谐檀,類似于基本數(shù)據(jù)類型抡谐。(如果執(zhí)行的是s1 = "abc",那么結(jié)果會(huì)都是true)
我們再看一個(gè)例子:
執(zhí)行第一行代碼: 在堆里分配空間存放String對象,在常量區(qū)開辟空間存放常量“abc”稚补,String對象指向常量童叠,s1指向該對象。
執(zhí)行第二行代碼:s2指向上一步new出來的string對象课幕。
執(zhí)行第三行代碼: 在堆里分配新的空間存放String對象厦坛,新對象指向常量“abc”,s3指向該對象乍惊。
到這里杜秸,很明顯,s1和s2指向的是同一個(gè)對象
接著就很詭異了润绎,我們讓s1 依舊= “abc",但是結(jié)果s1和s2指向的地址不同了撬碟。
怎么回事呢诞挨?這就是String類的特殊之處了,new出來的String不再是上面的字符串常量呢蛤,而是字符串對象惶傻。
由于String類是不可改變的,所以String對象也是不可改變的其障,我們每次給String賦值都相當(dāng)于執(zhí)行了一次new String()银室,然后讓變量指向這個(gè)新對象,而不是在原來的對象上修改励翼。
當(dāng)然蜈敢,java還提供了StringBuffer類,這個(gè)是可以在原對象上做修改的汽抚。如果你需要修改原對象抓狭,那么請使用StringBuffer類。
值傳遞和引用傳遞的戰(zhàn)爭
java是值傳遞還是引用傳遞的呢造烁?毫無疑問否过,java是值傳遞的。那么什么又叫值傳遞和引用傳遞呢膨蛮?
我們先來看一個(gè)例子:
這是一個(gè)很經(jīng)典的例子叠纹,我們希望調(diào)用了swap函數(shù)以后,a和b的值可以互換敞葛,但是事實(shí)上并沒有誉察。為什么會(huì)這樣呢?
這就是因?yàn)閖ava是值傳遞的惹谐。也就是說持偏,我們在調(diào)用一個(gè)需要傳遞參數(shù)的函數(shù)時(shí),傳遞給函數(shù)的參數(shù)并不是我們傳進(jìn)去的參數(shù)本身氨肌,而是它的副本鸿秆。說起來比較拗口,但是其實(shí)原理很簡單怎囚。我們可以這樣理解:
一個(gè)有形參的函數(shù)卿叽,當(dāng)別的函數(shù)調(diào)用它的時(shí)候,必須要傳遞數(shù)據(jù)恳守。
比如swap函數(shù)考婴,別的函數(shù)要調(diào)用swap就必須傳兩個(gè)整數(shù)過來。
這個(gè)時(shí)候催烘,有一個(gè)函數(shù)按耐不住寂寞沥阱,扔了兩個(gè)整數(shù)過來,但是伊群,swap函數(shù)有潔癖考杉,它不喜歡用別人的東西策精,于是它把傳過來的參數(shù)復(fù)制了一份,然后對復(fù)制的數(shù)據(jù)修修改改崇棠,而別人傳過來的參數(shù)動(dòng)根本沒動(dòng)咽袜。
所以,當(dāng)swap函數(shù)執(zhí)行完畢之后易茬,交換了的數(shù)據(jù)只是swap自己復(fù)制的那一份酬蹋,而原來的數(shù)據(jù)沒變及老。
也可以理解為別的函數(shù)把數(shù)據(jù)傳遞給了swap函數(shù)的形參抽莱,最后改變的只是形參而實(shí)參沒變,所以不會(huì)起到任何效果骄恶。
我們再來看一個(gè)復(fù)雜一點(diǎn)的例子(Person類添加了get食铐,set方法):
可以看到,我們把p1傳進(jìn)去僧鲁,它并沒有被替換成新的對象虐呻。因?yàn)閏hange函數(shù)操作的不是p1這個(gè)引用本身,而是這個(gè)引用的一個(gè)副本寞秃。
你依然可以理解為斟叼,主函數(shù)將p1復(fù)制了一份然后變成了chagne函數(shù)的形參,最終指向新Person對象的是那個(gè)副本引用春寿,而實(shí)參p1并沒有改變朗涩。
再來看一個(gè)例子:
這次為什么就改變了呢?分析一下绑改。
首先谢床,new了一個(gè)Person對象,暫且叫他小明吧厘线。然后p1指向小明识腿。
小明10歲了,隨著時(shí)間的推移造壮,小明的年齡要變了渡讼,調(diào)用了一下changgeAge方法,把小明的引用傳了進(jìn)去耳璧。
傳遞的過程中成箫,changgeAge也有潔癖,于是復(fù)制了一份小明的引用楞抡,這個(gè)副本也指向小明伟众。
然后changgeAge通過自己的副本引用,改變了小明的年齡召廷。
由于是小明這個(gè)對象被改變了凳厢,所以所有小明的引用調(diào)用方法得到的年齡都會(huì)改變
所以就變了账胧。
最后簡單的總結(jié)一下。
java的傳值過程先紫,其實(shí)傳的是副本治泥,不管是變量還是引用。所以遮精,不要期待把變量傳遞給一個(gè)函數(shù)來改變變量本身居夹。
對象的強(qiáng)引用,軟引用本冲,弱引用和虛引用
Java中是JVM負(fù)責(zé)內(nèi)存的分配和回收准脂,這樣雖然使用方便,程序不用再像使用c那樣操心內(nèi)存檬洞,但同時(shí)也是它的缺點(diǎn)(不夠靈活)狸膏。為了解決內(nèi)存操作不靈活這個(gè)問題,可以采用軟引用等方法添怔。
先介紹一下這四種引用:
- 強(qiáng)引用
以前我們使用的大部分引用實(shí)際上都是強(qiáng)引用湾戳,這是使用最普遍的引用。如果一個(gè)對象具有強(qiáng)引用广料,那就類似于必不可少的生活用品砾脑,垃圾回收器絕不會(huì)回收它。當(dāng)內(nèi)存空 間不足艾杏,Java虛擬機(jī)寧愿拋出OutOfMemoryError錯(cuò)誤韧衣,使程序異常終止,也不會(huì)靠隨意回收具有強(qiáng)引用的對象來解決內(nèi)存不足問題糜颠。
-
軟引用(SoftReference)
如果一個(gè)對象只具有軟引用汹族,那就類似于可有可物的生活用品。如果內(nèi)存空間足夠其兴,垃圾回收器就不會(huì)回收它顶瞒,如果內(nèi)存空間不足了,就會(huì)回收這些對象的內(nèi)存元旬。只要垃圾回收器沒有回收它榴徐,該對象就可以被程序使用。軟引用可用來實(shí)現(xiàn)內(nèi)存敏感的高速緩存匀归。
軟引用可以和一個(gè)引用隊(duì)列(ReferenceQueue)聯(lián)合使用坑资,如果軟引用所引用的對象被垃圾回收,JAVA虛擬機(jī)就會(huì)把這個(gè)軟引用加入到與之關(guān)聯(lián)的引用隊(duì)列中穆端。
-
弱引用(WeakReference)
如果一個(gè)對象只具有弱引用袱贮,那就類似于可有可物的生活用品。弱引用與軟引用的區(qū)別在于:只具有弱引用的對象擁有更短暫的生命周期体啰。在垃圾回收器線程掃描它 所管轄的內(nèi)存區(qū)域的過程中攒巍,一旦發(fā)現(xiàn)了只具有弱引用的對象嗽仪,不管當(dāng)前內(nèi)存空間足夠與否,都會(huì)回收它的內(nèi)存柒莉。不過闻坚,由于垃圾回收器是一個(gè)優(yōu)先級很低的線程, 因此不一定會(huì)很快發(fā)現(xiàn)那些只具有弱引用的對象兢孝。
弱引用可以和一個(gè)引用隊(duì)列(ReferenceQueue)聯(lián)合使用窿凤,如果弱引用所引用的對象被垃圾回收,Java虛擬機(jī)就會(huì)把這個(gè)弱引用加入到與之關(guān)聯(lián)的引用隊(duì)列中跨蟹。
-
虛引用(PhantomReference)
"虛引用"顧名思義雳殊,就是形同虛設(shè),與其他幾種引用都不同喷市,虛引用并不會(huì)決定對象的生命周期相种。如果一個(gè)對象僅持有虛引用,那么它就和沒有任何引用一樣品姓,在任何時(shí)候都可能被垃圾回收。
虛引用主要用來跟蹤對象被垃圾回收的活動(dòng)箫措。虛引用與軟引用和弱引用的一個(gè)區(qū)別在于:虛引用必須和引用隊(duì)列(ReferenceQueue)聯(lián)合使用腹备。當(dāng)垃 圾回收器準(zhǔn)備回收一個(gè)對象時(shí),如果發(fā)現(xiàn)它還有虛引用斤蔓,就會(huì)在回收對象的內(nèi)存之前植酥,把這個(gè)虛引用加入到與之關(guān)聯(lián)的引用隊(duì)列中。程序可以通過判斷引用隊(duì)列中是 否已經(jīng)加入了虛引用弦牡,來了解被引用的對象是否將要被垃圾回收友驮。程序如果發(fā)現(xiàn)某個(gè)虛引用已經(jīng)被加入到引用隊(duì)列,那么就可以在所引用的對象的內(nèi)存被回收之前采取必要的行動(dòng)驾锰。
在實(shí)際開發(fā)中卸留,弱引用和虛引用不常用,用得比較多的是軟引用椭豫,因?yàn)樗梢约铀賘vm的回收耻瑟。
軟引用的使用方式:
關(guān)于軟引用,我之后會(huì)單獨(dú)寫一篇文章赏酥,所以這里先一筆帶過喳整。
對象的復(fù)制
java除了用new來創(chuàng)建對象,還可以通過clone來復(fù)制對象裸扶。
那么這兩種方式有什么相同和不同呢框都?
- new
new操作符的本意是分配內(nèi)存。程序執(zhí)行到new操作符時(shí)呵晨,首先去看new操作符后面的類型魏保,因?yàn)橹懒祟愋驼崽#拍苤酪峙涠啻蟮膬?nèi)存空間。分配完內(nèi)存之后囱淋,再調(diào)用構(gòu)造函數(shù)猪杭,填充對象的各個(gè)域,這一步叫做對象的初始化妥衣,構(gòu)造方法返回后皂吮,一個(gè)對象創(chuàng)建完畢,可以把他的引用(地址)發(fā)布到外部税手,在外部就可以使用這個(gè)引用操縱這個(gè)對象蜂筹。
- clone
clone在第一步是和new相似的, 都是分配內(nèi)存芦倒,調(diào)用clone方法時(shí)艺挪,分配的內(nèi)存和源對象(即調(diào)用clone方法的對象)相同,然后再使用原對象中對應(yīng)的各個(gè)域兵扬,填充新對象的域麻裳, 填充完成之后,clone方法返回器钟,一個(gè)新的相同的對象被創(chuàng)建津坑,同樣可以把這個(gè)新對象的引用發(fā)布到外部。
如何利用clone的方式來得到一個(gè)對象呢傲霸?
看代碼:
對Person類做了一些修改
看實(shí)現(xiàn)代碼:
這樣就得到了一個(gè)和原來一樣的新對象疆瑰。
深復(fù)制和淺復(fù)制
但是,細(xì)心并且善于思考的人可能一經(jīng)發(fā)現(xiàn)了一個(gè)問題昙啄。
age是一個(gè)基本數(shù)據(jù)類型穆役,支架clone沒什么問題,但是name可是一個(gè)String類型的啊梳凛。我們clone后的對象里的name和原來對象的name是不是指向同一個(gè)字符串常量呢耿币?
做個(gè)試驗(yàn):
果然,是同一個(gè)對象伶跷。如果你不能理解掰读,那么看這個(gè)圖。
其實(shí)如果只是String還好叭莫,因?yàn)镾tring的不可變性蹈集,當(dāng)你隨便修改一個(gè)值的時(shí)候,他們就會(huì)指向不同的地址了雇初,但是除了String拢肆,其他都是可變的。這就危險(xiǎn)了。
上面的這種情況郭怪,就是淺克隆支示。這種方式在你的屬性列表中有其他對象的引用的時(shí)候其實(shí)是很危險(xiǎn)的。所以鄙才,我們需要深克隆颂鸿。也就是說我們需要將這個(gè)對象里的對象也clone一份。怎么做呢攒庵?
在內(nèi)存中通過字節(jié)流的拷貝是比較容易實(shí)現(xiàn)的嘴纺。把母對象寫入到一個(gè)字節(jié)流中,再從字節(jié)流中將其讀出來浓冒,這樣就可以創(chuàng)建一個(gè)新的對象了栽渴,并且該新對象與母對象之間并不存在引用共享的問題,真正實(shí)現(xiàn)對象的深拷貝稳懒。
//使用該工具類的對象必須要實(shí)現(xiàn) Serializable 接口闲擦,否則是沒有辦法實(shí)現(xiàn)克隆的。
public class CloneUtils {
public static <T extends Serializable> T clone(T obj){
T cloneObj = null;
try {
//寫入字節(jié)流
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream obs = new ObjectOutputStream(out);
obs.writeObject(obj);
obs.close();
//分配內(nèi)存场梆,寫入原始對象墅冷,生成新對象
ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(ios);
//返回生成的新對象
cloneObj = (T) ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
}
使用該工具類的對象只要實(shí)現(xiàn) Serializable 接口就可實(shí)現(xiàn)對象的克隆,無須繼承 Cloneable 接口實(shí)現(xiàn) clone() 方法辙谜。
測試一下:
很完美
這個(gè)時(shí)候俺榆,Person類實(shí)現(xiàn)了Serializable接口
是否使用復(fù)制,深復(fù)制還是淺復(fù)制看情況來使用装哆。
關(guān)于序列化與反序列化以后會(huì)講。
這篇文章到這里就暫時(shí)告一段落了定嗓,后續(xù)有補(bǔ)充的話我會(huì)繼續(xù)補(bǔ)充蜕琴,有錯(cuò)誤的話,我也會(huì)及時(shí)改正宵溅。歡迎大家提出問題凌简。
示例代碼放在github:https://github.com/CleverFan/JavaImprove