癡情研究java內(nèi)存中的對(duì)象
前記:
幾天前,在瀏覽網(wǎng)頁時(shí)偶然的發(fā)現(xiàn)一道以前就看過很多遍的面試題,,題目是:“請(qǐng)說出‘equals’和‘==’的區(qū)別”,,當(dāng)時(shí)我覺得我還是挺懂的,在心里答了一點(diǎn)(比如我們都知道的:‘==’比較兩個(gè)引用是否指向同一個(gè)對(duì)象,,‘equals’比較兩個(gè)對(duì)象的內(nèi)容),可是總覺得心里有點(diǎn)虛虛的,,因?yàn)檫@句話好像太概括了,,我也無法更深入地說出一些。于是看了幾篇?jiǎng)e人的技術(shù)博客,,看完后我心里自信地說,,我是真的懂了;后來根據(jù)我當(dāng)時(shí)的理解,,就在eclipse中敲了些代碼驗(yàn)證一下,,發(fā)現(xiàn)有些運(yùn)行的結(jié)果和我預(yù)期的又不一樣,怎么找原因都找不到,,呵呵~,,這時(shí)就感覺太傷自尊了,于是我覺得我真的還是不懂得,又去上網(wǎng)查找答案,,呵呵,,這個(gè)問題花了我整整三天的時(shí)間,現(xiàn)在想把我的一些總結(jié)寫下來,,以達(dá)到檢測(cè)自己的目的,,也歡迎大家瀏覽、批評(píng),、指正,。
注:本文不僅研究類類型的對(duì)象,還研究基本數(shù)據(jù)類型
線索:
我想采用實(shí)例代碼驅(qū)動(dòng)的方式來一步步地分析,,這也符合我們探知新事物的過程,。
一、基本數(shù)據(jù)類型的內(nèi)存分配
代碼1:
- int p1=1000;
- static int p2=1000;
- public void myTest(){
- System.err.println("****************Integer*********************");
- int i1=1000;
- int i2=1000;
- Integer i3=1000;
- Integer i4=1000;
- Integer i5=100;
- Integer i6=100;
- Integer i7=new Integer(1000);
- Integer i8=new Integer(1000);
- System.err.println(i1==p1); //true(輸出結(jié)果) 1(編號(hào),,便于分析)
- System.err.println(i1==p2); //true 2
- System.err.println(i1==i2); //true 3
- System.err.println(i3==i4); //false 4
- System.err.println(i5==i6); //true 5
- System.err.println(i7==i8); //false 6
- System.err.println(i1==i3); //true 7
- System.err.println(i1==i7); //true 8
- System.err.println(i3==i7); //false 9
- System.err.println("****************Integer*********************");
- }
看到上面的輸出結(jié)果,,如果你還是有些不能理解的,那就耐心地接著看我的分析吧,。
分析:
編號(hào)1:在java編譯時(shí)期,,當(dāng)編譯到“int p1=1000; ”時(shí)會(huì)在棧中壓入1000,其實(shí)后面的p2,i1,i2都是指向這個(gè)1000,,這樣可以提高java的性能,,所以編號(hào)1、編號(hào)2,、編號(hào)3的輸出結(jié)果都是true.其實(shí)char,float,double等基本數(shù)據(jù)類型都是這樣的,。
編號(hào)2、編號(hào)3:同編號(hào)1
編號(hào)4:這是java中的自動(dòng)裝箱機(jī)制,,將基本數(shù)據(jù)類型int自動(dòng)轉(zhuǎn)為類類型Integer,這是jdk1.5以上才有的功能,,jdk1.5以下編譯時(shí)會(huì)報(bào)錯(cuò)。自動(dòng)裝箱時(shí)java底層會(huì)調(diào)用Integer.valueOf(int i)方法自動(dòng)裝箱,,下面我們來看看Integer.valueOf(int i)的源碼吧:
- /**
- * @param i an <code>int</code> value.
- * @return a <tt>Integer</tt> instance representing <tt>i</tt>.
- * @since 1.5
- */
- public static Integer valueOf(int i) {
- if(i >= -128 && i <= IntegerCache.high)
- return IntegerCache.cache[i + 128];
- else
- return new Integer(i);
- }
注:分析源碼我們知道IntegerCache.high其實(shí)就是127,,在IntergerCache的靜態(tài)塊中定義的。
源碼的意思是當(dāng)i的值在-128—127之間時(shí)會(huì)返回IntegerCache.cache[]中的對(duì)象,,其他的新建一個(gè)Integer對(duì)象,。其實(shí)Integer類是這樣實(shí)現(xiàn)的:考慮到-128—127之間的對(duì)象經(jīng)常使用,就在Integer創(chuàng)建時(shí)將值在-128—127之間的對(duì)象先創(chuàng)建好,,放在池中,,以后要使用時(shí),這些對(duì)象就不用重新創(chuàng)建了,,目的在于提高性能,。其實(shí)這種機(jī)制在Character中也用到了,,Character是創(chuàng)建ASCII在0—127之間的對(duì)象。補(bǔ)充說明:Integer創(chuàng)建的對(duì)象引用在棧中,,對(duì)象的內(nèi)容在堆區(qū),,棧中的值是堆中對(duì)象的地址。Character,、Long,、Short等包裝類都是這樣的。所以編號(hào)4的輸出結(jié)果是false,因?yàn)橹荡笥?27,,java新創(chuàng)建了一個(gè)對(duì)象,。
編號(hào)5:因?yàn)橹翟?128—127之間,所以兩個(gè)引用指向的是堆區(qū)的同一個(gè)對(duì)象,。
編號(hào)6:當(dāng)使用new創(chuàng)建對(duì)象時(shí),,都會(huì)新創(chuàng)建一個(gè)對(duì)象,即在棧中創(chuàng)建一個(gè)引用,,在堆中創(chuàng)建該對(duì)象,,引用指向?qū)ο蟆?/span>
編號(hào)7:這種情況有些人可能會(huì)不太清楚,其實(shí)這是java的自動(dòng)拆箱機(jī)制,,當(dāng)int和Integer發(fā)生操作時(shí),,Integer類型對(duì)象會(huì)自動(dòng)拆箱成int值,這時(shí)比較的是兩個(gè)int值,,而我們前面分析了,,int值都會(huì)指向常量池中的數(shù)據(jù),所以,,兩者指向的是同一塊空間,。結(jié)果編號(hào)7輸出true
編號(hào)8:同編號(hào)7,也是Integer的自動(dòng)拆箱,。
編號(hào)9:我想,,分析了這么多,編號(hào)9不用我說,,你也應(yīng)該懂了,,呵呵,這里就不贅述了哦~
分析了這么多,,終于第一塊代碼分析完了。
二,、String類型的內(nèi)存分配
大家都知道String類型是類類型,,不過String類型是一個(gè)特殊的類類型,那它特殊在哪呢,? 代碼2:
- System.err.println("****************string*********************");
- String s1="abc";
- String s2="abc";
- String s3=new String("abc");
- String s4=new String("abc");
- System.err.println(s1==s2);//true (輸出結(jié)果) 1(編號(hào))
- System.err.println(s3==s4);//false 2
- System.err.println(s1==s3);//false 3
-
- String a = "abc";
- String b = "ab";
- String c = b + "c";
- System.err.println(a==c);//false 4
-
- String s5 = "123";
- final String s6="12";
- String s7=s6+"3";
- System.err.println(s5 == s7);//true 5
- System.err.println("****************string*********************");
編號(hào)1:String類型是一個(gè)很特殊的類型,,當(dāng)我們使用String str=”abc”;這種定義方法時(shí),”abc”會(huì)放入常量池中,以后如果再有定義String str2=”abc”時(shí),,其實(shí)str和str2指向的是常量池中同一個(gè)對(duì)象,。而只有當(dāng)使用new創(chuàng)建時(shí)才會(huì)每次都創(chuàng)建一個(gè)新的對(duì)象。(我覺得這是String類型和其他類類型的特殊之處)
編號(hào)2,、編號(hào)3:編號(hào)1已經(jīng)分析了,。
編號(hào)4:執(zhí)行到 String c = b + "c"; 這一句時(shí),java底層會(huì)先創(chuàng)建一個(gè)StringBuilder對(duì)象,,封裝b,接著再加上“c”,,最后再創(chuàng)建一個(gè)String對(duì)象,將StringBuilder中的值賦給該String對(duì)象,,用c來指向它,。.其實(shí)此時(shí)的c指向的對(duì)象已經(jīng)不是a指向的對(duì)象了。
編號(hào)5:當(dāng)用final修飾后,,s6就變?yōu)榱顺A?,在常量池中?chuàng)建“12”,當(dāng)執(zhí)行到String s7=s6+"3";時(shí),,編譯器直接就把s6當(dāng)成了“12”,,s7此時(shí)就已是“123”,它指向常量池中的“123”,,所以s5和s7指向的是同一個(gè)對(duì)象,,輸出為true。
三,、StringBuilder,StringBuffer,String的對(duì)比
(一)String
String類型的值是不可變的,,聽到這句話后可能你會(huì)有疑問,我們的String對(duì)象可以重新賦值呀,,這里有兩種情況,,情況一:String str=”abc”; , 情況二:String str=new String(“abc”);采用情況一重新賦值時(shí),java會(huì)先看常量池中有沒有“abc”,,如果有則直接指向它,,如果沒有,在編譯時(shí)就創(chuàng)建一個(gè)常量放入常量池中,;對(duì)于情況二:str則重新指向一個(gè)先創(chuàng)建的對(duì)象,,該新對(duì)象在堆中。下面提出問題:為什么String是不可變的呢,?我們來看看String的源碼:
- public final class String
- implements java.io.Serializable, Comparable<String>, CharSequence
- {
- /** The value is used for character storage. */
- private final char value[];
-
- /** The offset is the first index of the storage that is used. */
- private final int offset;
-
- /** The count is the number of characters in the String. */
- private final int count;
- //····················
我們看到String類型是用一個(gè)用final修飾的char數(shù)組來存儲(chǔ)字符串的,,所以String類型是不可變的,(其實(shí)Short,,Character,,Long等包裝類型也是這樣實(shí)現(xiàn)的),,根據(jù)上面對(duì)String類型的分析,如果要改變String的值,,就要重新創(chuàng)建一個(gè)對(duì)象,,這無疑性能會(huì)很差。為了優(yōu)化String,,sun公司添加了StringBuffer,,在jdk1.5之后又添加了StringBuilder。
(二)下面我們來分析一下StringBuffer
StringBuffer作為字符串緩沖類,,當(dāng)進(jìn)行字符串拼接時(shí),,不會(huì)重新創(chuàng)建一個(gè)StringBuffer對(duì)象,而是直接在原有值后面添加,,因?yàn)镾tringBuffer類繼承了AbstractStringBuffer類,,分析后者的源碼后,我們發(fā)現(xiàn)存儲(chǔ)字符串的char[]沒有被final修飾,。至于StringBuffer類是怎樣擴(kuò)充自己的長(zhǎng)度的,,我們可以參考它的append()方法,這里不再贅述,。不過一定要提出的是:StringBuffer是線程安全的,,它的方法體是被synchronized修飾了的。
(三)StringBuilder有是怎么樣的呢,?
StringBuilder基本實(shí)現(xiàn)了StringBuffer的功能,,最大的不同之處在于StringBuilder不是線程安全的。
(四)String,、StringBuffer,、StringBuilder的性能比較
代碼三:
- StringBuffer b = new StringBuffer("abc");
- long t3 = System.currentTimeMillis();
- for (int i = 0; i < 1000000; i++) {
- b = b.append("def");
- }
- long t4 = System.currentTimeMillis();
- System.out.println("1000000次拼接,StringBuffer所花時(shí)間為:" + (t4 - t3));
- System.out.println("*************************************");
- StringBuilder c = new StringBuilder("abc");
- long t5 = System.currentTimeMillis();
- for (int i = 0; i < 1000000; i++) {
- c = c.append("def");
- }
- long t6 = System.currentTimeMillis();
- System.out.println("1000000次拼接,,StringBuilder所花時(shí)間為:" + (t6 - t5));
- System.out.println("*************************************");
- String S1 = "abc";
- long t1 = System.currentTimeMillis();
- for (int i = 0; i < 10000; i++) {
- S1 += "def";
- }
- long t2 = System.currentTimeMillis();
- System.out.println("10000次拼接,,String所花時(shí)間為:" + (t2 - t1));
實(shí)驗(yàn)結(jié)果為:
- 1000000次拼接,StringBuffer所花時(shí)間為:203
- *************************************
- 1000000次拼接,,StringBuilder所花時(shí)間為:79
- *************************************
- 10000次拼接,,String所花時(shí)間為:640
顯然,StringBuilder的性能最好,,String的性能最差,,而且差很多;不過StringBuffer的線程安全性很好,,性能也比較接近StringBuilder,所以我推薦的選擇使用順序?yàn)椋篠tringBuffer>StringBuilder>String;
四,、java傳參
下面我們我看一段代碼,不過有點(diǎn)長(zhǎng),,請(qǐng)大家有點(diǎn)耐心哦~ 代碼四:
- public class VariableTest {
- public static void main(String[] args) {
- VariableTest t = new VariableTest();
- t.test();
- }
-
- class Point {
- int x;
- String y;
- StringBuffer sb;
- public Point(int x, String y, StringBuffer sb) {
- this.x = x;
- this.y = y;
- this.sb = sb;
- }
- }
-
- public void test() {
- int i = 1;
- String str = "abc";
- StringBuffer bs = new StringBuffer("abc");
- Point p = new Point(1, "2", new StringBuffer("abc"));
- System.out.println("***********函數(shù)調(diào)用之前****************");
- System.out.println("i為:" + i);
- System.out.println("str為:" + str);
- System.out.println("bs為:" + bs);
- System.out.println("p的x為:" + p.x + " p的y為:" + p.y
- + " p的sb為:" + p.sb);
- change(i, str, bs, p);
- System.out.println("***********函數(shù)調(diào)用之后****************");
- System.out.println("i為:" + i);
- System.out.println("str為:" + str);
- System.out.println("bs為:" + bs);
- System.out.println("p的x為:" + p.x + " p的y為:" + p.y
- + " p的sb為:" + p.sb);
- }
-
- public void change(int p1, String p2, StringBuffer p3, Point p4) {
- p1 = 2;
- p2 = "I have changed!";
- p3 = p3.append(" I have changed!");
- p4.x = 5;
- p4.y = "I have changed!";
- p4.sb = p4.sb.append(" I have changed!");
- }
-
- }
輸出結(jié)果為:
- ***********函數(shù)調(diào)用之前****************
- i為:1
- str為:abc
- bs為:abc
- p的x為:1 p的y為:2 p的sb為:abc
- ***********函數(shù)調(diào)用之后****************
- i為:1
- str為:abc
- bs為:abc I have changed!
- p的x為:5 p的y為:I have changed! p的sb為:abc I have changed!
分析:
這個(gè)例子我舉得有點(diǎn)大,,不過我覺得如果把我舉得這個(gè)例子的參數(shù)傳遞完全搞懂了,你對(duì)java的參數(shù)傳遞過程就比較了解了,。
不過在分析之前,,我想給大家java傳參的一個(gè)思想:java只有值傳遞,沒有引用傳遞,,也沒有指針傳遞,。對(duì)于基本數(shù)據(jù)類型,java是直接傳值,,其實(shí)就是將形參指向棧中的那個(gè)值,;對(duì)于類類型(比如String,StringBuffer,,自定義類類型等)是傳引用(在棧中)的值,,也就是堆中對(duì)應(yīng)對(duì)象的地址。這個(gè)在我認(rèn)為也是值傳遞,。
下面我們開始分析test()方法
1,、首先定義了int類型變量,int類型變量傳入change()方法是簡(jiǎn)單的值傳遞,,這個(gè)大家都知道,,所以就不說了;
2,、下面是String類型的變量,,大家可能會(huì)想,String類型是類類型啊,,當(dāng)調(diào)用change方法后test方法中也應(yīng)該會(huì)發(fā)生變化呀,,呵呵,其實(shí)這時(shí)你忘了String類型是不可變的,,因?yàn)樗鎯?chǔ)數(shù)據(jù)的char[]是用final修飾過的,。當(dāng)change方法中改變了p2的值后,其實(shí)p2指向的已經(jīng)是另一塊內(nèi)存空間了,。
3,、下面是StringBuffer類型,之前已說類類型傳遞變量的地址,,所以bs和p3指向的是同一塊內(nèi)存空間,,當(dāng)p3重新賦值時(shí),bs也會(huì)跟著變得,。
4,、下面是自定義的類類型,我不想再用文字述說了,,就用一個(gè)圖來表示吧,,我相信你現(xiàn)在可以自己分析了,。
五、java對(duì)象的克隆機(jī)制(以上概念的應(yīng)用)
概念引入:
我相信大家都聽過java中的“克隆”這個(gè)名詞,,在Object類中有一個(gè)本地化clone()方法就是用來克隆對(duì)象的,,其實(shí)我們自己也可以用new來克隆對(duì)象,但這樣的效率會(huì)比較低,。
概念名詞:
淺度克?。阂寺?duì)象的屬性如果是類類型變量,只在棧中創(chuàng)建一個(gè)該屬性的新引用,,指向源屬性對(duì)象,;如果是基本數(shù)據(jù)類型,我相信你懂得,。
深度克?。簩?duì)于類類型的屬性,在棧中和堆中都重新開辟空間,,創(chuàng)建一個(gè)全新的屬性對(duì)象,。
其實(shí)Object中的clone()方法就是一種淺度克隆,不過當(dāng)我們重寫該方法時(shí)一定要實(shí)現(xiàn)Cloneable接口,,否則會(huì)報(bào)異常,,代碼驗(yàn)證如下: 代碼五:
- public class CloneTest {
-
- public static void main(String[] args) {
- // TODO Auto-generated method stub
- Point p1 = new CloneTest().new Point(1, "abc", new StringBuffer("def"));//源對(duì)象
- Point p2=p1.clone(); //克隆對(duì)象
- System.out.println("*************源對(duì)象的值如下****************");
- System.out.println(p1.x);
- System.out.println(p1.y);
- System.out.println(p1.sb);
- System.out.println("************修改克隆對(duì)象的值*****************");
- p2.x=2;
- p2.y="ddddddd";
- p2.sb=p2.sb.append("dfsfdsfsd");
- System.out.println("************修改克隆對(duì)象的值后 ,源對(duì)象的值如下*****************");
- System.out.println(p1.x);
- System.out.println(p1.y);
- System.out.println(p1.sb);
- }
-
- /**
- * 內(nèi)部類,,用于克隆實(shí)驗(yàn)
- */
- class Point implements Cloneable{
- int x;
- String y;
- StringBuffer sb;
- //構(gòu)造方法
- public Point(int x, String y, StringBuffer sb) {
- this.x = x;
- this.y = y;
- this.sb = sb;
- }
-
- /**
- * 重寫Object類的clone方法,,不過默認(rèn)情況下只能淺克隆,不過我們可以給類類型的變量
- * 重新new一塊空間實(shí)現(xiàn)深度克隆,,String類型就不用了哦~ ,,呵呵,如果你現(xiàn)在還不知道
- * 為什么,,那就把博客再看一遍吧,,我充分相信你會(huì)懂得,這里我不想再贅述了,,總之要知道,,String
- * 類型和其他的類類型總是有一些區(qū)別,看到現(xiàn)在我希望你可以總結(jié)出一些
- */
- public Point clone(){
- Point o=null;
- try {
- o = (Point)super.clone();
- //o.sb=new StringBuffer(); //實(shí)現(xiàn)深度克隆
- } catch (CloneNotSupportedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } return o;
- }
- }
- }
這時(shí)的運(yùn)行結(jié)果如下,,很顯然是淺克隆,。
- *************源對(duì)象的值如下****************
- 1
- abc
- def
- ************修改克隆對(duì)象的值*****************
- ************修改克隆對(duì)象的值后 ,源對(duì)象的值如下*****************
- 1
- abc
- defdfsfdsfsd
當(dāng)我們把clone()方法中的注釋語句“//o.sb=new StringBuffer(); ”啟用后,,這就是深度克隆了哦,,運(yùn)行結(jié)果如下:
- *************源對(duì)象的值如下****************
- 1
- abc
- def
- ************修改克隆對(duì)象的值*****************
- ************修改克隆對(duì)象的值后 ,源對(duì)象的值如下*****************
- 1
- abc
- def
上面實(shí)現(xiàn)深度克隆的方法是基于Object的clone()方法的,其實(shí)我們也可以采用序列化的方式來實(shí)現(xiàn)深度克隆的,,這樣就不用重寫clone()方法了,,我們給Point類添加一個(gè)deepClone方法,不過一定要讓Point類實(shí)現(xiàn)Serializeble接口哦~,deepClone方法如下:
- /**
- * 采用序列化的方式實(shí)現(xiàn)深度克隆
- */
- public Point deepClone() throws IOException, ClassNotFoundException {
- //將對(duì)象寫入流中
- ByteArrayOutputStream bs= new ByteArrayOutputStream();
- ObjectOutputStream os = new ObjectOutputStream(bs);
- os.writeObject(this);
- //從流中讀取對(duì)象
- ByteArrayInputStream is= new ByteArrayInputStream(bs.toByteArray());
- ObjectInputStream ois=new ObjectInputStream(is);
- return (Point) ois.readObject();
- }
呵呵,,通過這些實(shí)驗(yàn),,我想你對(duì)java的克隆機(jī)制還是比較了解了,具體的分析我也沒有必要再說了,。就到此為止吧···
如需轉(zhuǎn)載,請(qǐng)注明出處:http://weixiaolu./blog/1290821
|