你必須瞭解的Java:獲取對象的四種方式

上一節講了獲取類,也就是獲取class文件的三種方式,這一節主要講獲取對象的四種方式.

獲取對象的四種方式包括有:

  1. new class
  2. clone (最大的區別就是,有沒有複製對象,在堆內存中的是否爲一個)
  3. reflect(最容易想到的就是 框架中的工廠模式創建了對象)
  4. deserialization

new

這是我們最常用的方式,生成的對象置於內存中的堆空間中,堆空間的構成,一個old區,一個eden區,兩個survivor區。通常生成的對象會置於Eden區中,但是當生成的對象過大,超過jvm設置的一個值的時候,也會將該對象直接置於old區中。具體的關於創建對象時,jvm對於內存分配以及內存回收的相關知識,這裏也就不再累述了。

clone 克隆 (利用clone複製對象,完成生成對象。)

利用clone,在內存中進行數據塊的拷貝,複製已有的對象,也是生成對象的一種方式。前提是類實現Cloneable接口,Cloneable接口沒有任何方法,是一個空接口,也可以稱這樣的接口爲標誌接口,只有實現了該接口,纔會支持clone操作。有的人也許會問了,java中的對象都有一個默認的父類Object。

new 與 clone 的區別

首先看 複製引用 和 複製對象 的區別:

  1. 複製引用:下面p和p1只是引用而已,他們都指向了一個相同的對象Person(23, “zhang”) 。 可以把這種現象叫做引用的複製。
Person p = new Person(23, "zhang");
		Person p1 = p;
		System.out.println(p);
		System.out.println(p1)

在這裏插入圖片描述

  1. 複製對象:

    Person p = new Person(23, "zhang");  
    Person p1 = (Person) p.clone();
    

    [外鏈圖片轉存失敗(img-Sauj3Vw1-1569330583627)(img\2.png)]

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

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

淺複製

被複制對象的所有變量都含有與原來的對象相同的值,而所有的對其他對象的引用仍然指向原來的對象。換言之,淺複製僅僅複製所考慮的對象,而不復制它所引用的對象。

深複製

被複制對象的所有變量都含有與原來的對象相同的值,除去那些引用其他對象的變量。那些引用其他對象的變量將指向被複制過的新對象,而不再是原有的那些被引用的對象。換言之,深複製把要複製的對象所引用的對象都複製了一遍。

[外鏈圖片轉存失敗(img-Ev6MVzC5-1569330583628)(img\3.png)]

下面通過代碼進行驗證。如果兩個Person對象的name的地址值相同, 說明兩個對象的name都指向同一個String對象, 也就是淺拷貝, 而如果兩個對象的name的地址值不同, 那麼就說明指向不同的String對象, 也就是在拷貝Person對象的時候, 同時拷貝了name引用的String對象, 也就是深拷貝。驗證代碼如下:

Person p = new Person(23, "zhang");
Person p1 = (Person) p.clone();

String result = p.getName() == p1.getName() ? "clone是淺拷貝的" : "clone是深拷貝的";
	
System.out.println(result);

打印結果是:clone是淺拷貝的;

所以,clone方法執行的是淺拷貝, 在編寫程序時要注意這個細節。

現在爲了要在clone對象時進行深拷貝, 那麼就要Clonable接口,覆蓋並實現clone方法,除了調用父類中的clone方法得到新的對象, 還要將該類中的引用變量也clone出來。如果只是用Object中默認的clone方法,是淺拷貝的,再次以下面的代碼驗證:

static class Body implements Cloneable{
		public Head head;
		
		public Body() {}
 
		public Body(Head head) {this.head = head;}
 
		@Override
		protected Object clone() throws CloneNotSupportedException {
			return super.clone();
		}
	}

	static class Head /*implements Cloneable*/{
		public  Face face;
		
		public Head() {}
		public Head(Face face){this.face = face;}	
	}
public static void main(String[] args) throws CloneNotSupportedException {
		
		Body body = new Body(new Head());
		
		Body body1 = (Body) body.clone();
		
		System.out.println("body == body1:"+(body == body1));
		
		System.out.println("body.head == body1.head:" + (body.head == body1.head));	
	}

結果是:

在以上代碼中, 有兩個主要的類, 分別爲Body和Head, 在Body類中, 組合了一個Head對象。當對Body對象進行clone時它組合的Face對象只進行淺拷貝。打印結果可以驗證該結論:
body == body1 : false 也就是 相對於Body 類來說, 是深克隆的,但是對於裏面的參數,組合的對象Head,還只是淺克隆;

body.head == body1.head : true

如果要使Body對象在clone時進行深拷貝, 那麼就要在Body的clone方法中,將源對象引用的Head對象也clone一份。

打印結果爲: body == body1 : false body.head == body1.head : false

具體瞭解請參閱:https://blog.csdn.net/zhangjg_blog/article/details/18369201/

結論:

拷貝也算是我們經常使用的一個方法,但是如果是不明白其中原理的程序員可能還是會入坑的。下面總結幾條使用建議:
1.一定要實現Cloneable接口
2.複寫clone()方法,注意:默認是淺拷貝,這裏需要將引用類型進行深拷貝處理
3.特殊:String類雖然是引用類型,但是是final類,同時也有字符串常量池的存在,不必進行處理

分享一下在AngularJS 中的一個深克隆:

//添加列值  
addColumn=function(list,columnName,conlumnValues){ 
 	//新的集合 
    var newList=[];
 	for(var i=0;i<list.length;i++){ 
 	 	var oldRow= list[i]; 
 	 	for(var j=0;j<conlumnValues.length;j++){ 
 	 	 	var newRow= JSON.parse( JSON.stringify( oldRow )  );//深克隆 
 	 	 	newRow.spec[columnName]=conlumnValues[j]; 
 	 	 	newList.push(newRow); 
 	 	}      	  
 	}   	 
 	return newList; 
} 

JavaScript 中的深克隆

var newRow= JSON.parse( JSON.stringify( oldRow )  );	//深克隆 

這邊是將Java對象先轉化爲字符串,再將自該字符串轉化爲對象;這個過程就是將對象進行了深克隆;

關於克隆的知識積累

利用clone,在內存中進行數據塊的拷貝,複製已有的對象,也是生成對象的一種方式。前提是類實現Cloneable接口,Cloneable接口沒有任何方法,是一個空接口,也可以稱這樣的接口爲標誌接口,只有實現了該接口,纔會支持clone操作。有的人也許會問了,java中的對象都有一個默認的父類Object。

Object中有一個clone方法,爲什麼還必須要實現Cloneable接口呢,這就是Cloneable接口這個標誌接口的意義,只有實現了這個接口才能實現複製操作,因爲jvm在複製對象的時候,會檢查對象的類是否實現了Cloneable這個接口,如果沒有實現,則會報CloneNotSupportedException異常。類似這樣的接口還有Serializable接口、RandomAccess接口等。

還有值得一提的是在執行clone操作的時候,不會調用構造函數。還有clone操作還會面臨深拷貝和淺拷貝的問題。由於通過複製操作得到對象不需要調用構造函數,只是內存中的數據塊的拷貝,那是不是拷貝對象的效率是不是一定會比new的時候的快。答案:不是。顯然jvm的開發者也意識到通過new方式來生成對象佔據了開發者生成對象的絕大部分,所以對於利用new操作生成對象進行了優化。

反射 獲取對象

還有所謂的工廠創建對象都是根據反射而創建的框架產生的,而其中的原理就是利用了反射。

上篇博客已經講過:反射是所有框架的基礎。

從本地文件的反序列化

首先,基本概念如下:

  1. 序列化 :把對象轉換爲字節序列存儲於磁盤或者進行網絡傳輸的過程稱爲對象的序列化。
  2. 反序列化:把磁盤或網絡節點上的字節序列恢復到對象的過程稱爲對象的反序列化。
  3. 綜上,可以得出對象的序列化和反序列化主要有兩種用途:
    • 把對象的字節序列永久地保存到磁盤上。(持久化對象)
    • 可以將Java對象以字節序列的方式在網絡中傳輸。(網絡傳輸對象)

【1】、必須實現序列化接口 SerializableJava.io.Serializable 接口。

【2】、serialVersionUID:序列化的版本號,凡是實現 Serializable 接口的類都有一個靜態的表示序列化版本標識符的變量。

【3】、serialVersionUID 的取值:此值是通過 Java 運行時環境根據類的內部細節自動生成的。如果類的源代碼進行了修改, 再重新編譯,新生成的類文件的 serialVersionUID 的值也會發生變化。不同的編譯器也可能會導致不同的 serialVersionUID。爲了提高 serialVersionUID 的獨立性和確定性,建議在一個序列化類中顯示的定義 serialVersionUID,爲它賦予明確的值。

:此值是通過 Java 運行時環境根據類的內部細節自動生成的。如果類的源代碼進行了修改, 再重新編譯,新生成的類文件的 serialVersionUID 的值也會發生變化。不同的編譯器也可能會導致不同的 serialVersionUID。爲了提高 serialVersionUID 的獨立性和確定性,建議在一個序列化類中顯示的定義 serialVersionUID,爲它賦予明確的值。

具體可以看https://blog.csdn.net/ShuSheng0007/article/details/80629348

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章