万物皆对象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是值傳遞的。也就是說,我們在調用一個須要傳遞參數的函數時,傳遞給函數的參數并非我們傳進去的參數本身。而是它的副本。說起來比較拗口,可是事實上原理非常easy。我們能夠這樣理解:
一個有形參的函數。當別的函數調用它的時候。必須要傳遞數據。
比方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一份。怎么做呢?
在內存中通過字節流的拷貝是比較easy實現的。把母對象寫入到一個字節流中,再從字節流中將其讀出來,這樣就能夠創建一個新的對象了。而且該新對象與母對象之間并不存在引用共享的問題,真正實現對象的深拷貝。
//使用該工具類的對象必須要實現 Serializable 接口,否則是沒有辦法實現克隆的。
public class CloneUtils {
public static 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集成redis集群_spring