一:概念
淺拷貝:創建了一個對象,但是這個對象的某些內容(比如A)依然是被拷貝對象的,即通過這兩個對象中任意一個修改A,兩個對象的A都會受到影響
深拷貝:相當於創建了一個新的對象,只是這個對象的所有內容,都和被拷貝的對象一模一樣而已,但是兩者是相互獨立的,是不同的地址值,其修改是隔離的,相互之間沒有影響
淺拷貝只複製指向某個對象的指針,而不復制對象本身,新舊對象還是共享同一塊內存。但深拷貝會另外創造一個一模一樣的對象,新對象跟原對象不共享內存,修改新對象不會改到原對象。
二:數據類型的比較
數據類型分爲:基本數據類型 與 引用數據類型
基本數據類型:四類八種(byte ,short,int,long double,float,char,boolean)
引用數據類型:引用數據類型有:類、接口類型、數組類型、枚舉類型、註解類型。
三:在內存中的位置:
1:基本數據類型:直接存儲在棧(stack)中的數據
2:引用數據類型:棧中存儲的是該對象的引用(是地址值),真實的數據存放在堆內存裏
3:賦值與淺拷貝的區別:
當我們把一個對象賦值給一個新的變量時,賦的其實是該對象的在棧中的地址,而不是堆中的數據。也就是兩個對象指向的是同一個存儲空間,無論哪個對象發生改變,其實都是改變的存儲空間的內容,因此,兩個對象是聯動的。
淺拷貝是按位拷貝對象,它會創建一個新對象,這個對象有着原始對象屬性值的一份精確拷貝。如果屬性是基本類型,拷貝的就是基本類型的值;如果屬性是內存地址(引用類型),拷貝的就是內存地址 ,因此如果其中一個對象改變了這個地址,就會影響到另一個對象。即默認拷貝構造函數只是對對象進行淺拷貝複製(逐個成員依次拷貝),即只複製對象空間而不復制資源。
四:實現對象克隆的幾個方式
(1)將A對象的值分別通過set方法加入B對象中;
(2)通過重寫java.lang.Object類中的方法clone();
(4)通過序列化實現對象的複製。
代碼舉例:
1>set賦值方式:
Student stu1 = new Student();
stu1.setNumber(12345);
Student stu2 = new Student();
stu2.setNumber(stu1.getNumber());
2>重寫克隆方法clone 需要實現cloneable接口
淺克隆:不支持引用數據類型的複製
被複制的類需要實現Clonenable接口(不實現的話在調用clone方法會拋出CloneNotSupportedException異常),
該接口爲標記接口(不含任何方法)
覆蓋clone()方法,訪問修飾符設爲public。方法中調用super.clone()方法得到需要的複製對象。(native爲本地方法)
class Student implements Cloneable{
private int number;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
@Override
public Object clone() {
Student stu = null;
try{
stu = (Student)super.clone();
}catch(CloneNotSupportedException e) {
e.printStackTrace();
}
return stu;
}
}
public class Test {
public static void main(String args[]) {
Student stu1 = new Student();
stu1.setNumber(12345);
Student stu2 = (Student)stu1.clone();
System.out.println("學生1:" + stu1.getNumber());
System.out.println("學生2:" + stu2.getNumber());
stu2.setNumber(54321);
System.out.println("學生1:" + stu1.getNumber());
System.out.println("學生2:" + stu2.getNumber());
}
}
在淺克隆中,如果原型對象的成員變量是值類型,將複製一份給克隆對象;如果原型對象的成員變量是引用類型,則將引用對象的地址複製一份給克隆對象,也就是說原型對象和克隆對象的成員變量指向相同的內存地址。
簡單來說,在淺克隆中,當對象被複制時只複製它本身和其中包含的值類型的成員變量,而引用類型的成員對象並沒有複製。
3>深克隆代碼對比舉例(先淺克隆後深克隆)
class Address {
private String add;
public String getAdd() {
return add;
}
public void setAdd(String add) {
this.add = add;
}
}
class Student implements Cloneable{
private int number;
private Address addr;
public Address getAddr() {
return addr;
}
public void setAddr(Address addr) {
this.addr = addr;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
@Override
public Object clone() {
Student stu = null;
try{
stu = (Student)super.clone(); //淺複製
}catch(CloneNotSupportedException e) {
e.printStackTrace();
}
return stu;
}
}
public class Test {
public static void main(String args[]) {
Address addr = new Address();
addr.setAdd("杭州市");
Student stu1 = new Student();
stu1.setNumber(123);
stu1.setAddr(addr);
Student stu2 = (Student)stu1.clone();
System.out.println("學生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
System.out.println("學生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
addr.setAdd("西湖區");
System.out.println("學生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
System.out.println("學生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
}
}
結果:
學生1:123,地址:杭州市
學生2:123,地址:杭州市
學生1:123,地址:西湖區
學生2:123,地址:西湖區
深克隆
class Address implements Cloneable {
private String add;
public String getAdd() {
return add;
}
public void setAdd(String add) {
this.add = add;
}
@Override
public Object clone() {
Address addr = null;
try{
addr = (Address)super.clone();
}catch(CloneNotSupportedException e) {
e.printStackTrace();
}
return addr;
}
}
class Student implements Cloneable{
private int number;
private Address addr;
public Address getAddr() {
return addr;
}
public void setAddr(Address addr) {
this.addr = addr;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
@Override
public Object clone() {
Student stu = null;
try{
stu = (Student)super.clone(); //淺複製
}catch(CloneNotSupportedException e) {
e.printStackTrace();
}
stu.addr = (Address)addr.clone(); //深度複製
return stu;
}
}
public class Test {
public static void main(String args[]) {
Address addr = new Address();
addr.setAdd("");
Student stu1 = new Student();
stu1.setNumber(123);
stu1.setAddr(addr);
Student stu2 = (Student)stu1.clone();
System.out.println("學生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
System.out.println("學生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
addr.setAdd("西湖區");
System.out.println("學生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
System.out.println("學生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
}
}
結果:
學生1:123,地址:杭州市
學生2:123,地址:杭州市
學生1:123,地址:西湖區
學生2:123,地址:杭州市
在深克隆中,無論原型對象的成員變量是值類型還是引用類型,都將複製一份給克隆對象,深克隆將原型對象的所有引用對象也複製一份給克隆對象。
簡單來說,在深克隆中,除了對象本身被複制外,對象所包含的所有成員變量也將複製。
4>:使用序列化和反序列化方式實現對象深度克隆
使用Serializable接口
利用序列化機制實現深克隆相比較重寫clone()方法來說要安全、簡單,但是效率不高。因爲clone()方法實現的克隆是利用的本地方法,效率比基於Java虛擬機規範的序列化機制要高很多。
1.對象實現序列化,其和成員對象類都需要實現Serializable接口,標記接口,開啓序列化,沒有提供任何方法。
2.利用ObjectInputStream和ObjectOutputStream實現對象的反序列化與序列化
序列化與反序列化概念:
序列化:把對象轉換爲字節序列的過程稱爲對象的序列化
反序列化:把字節序列恢復爲對象的過程稱爲對象的反序列化
什麼時候會用到序列化呢?一般在以下的情況中會使用到序列化
===對象的持久化:把對象的字節序列永久地保存到硬盤上,通常存放在一個文件中
在很多應用中,需要對某些對象進行序列化,讓它們離開內存空間,入住物理硬盤,以便長期保存。比如最常見的是Web服務器中的Session對象,當有 10萬用戶併發訪問,就有可能出現10萬個Session對象,內存可能喫不消,於是Web容器就會把一些seesion先序列化到硬盤中,等要用了,再把保存在硬盤中的對象還原到內存中。
===遠程調用:在網絡上傳送對象的字節序列
當兩個進程在進行遠程通信時,彼此可以發送各種類型的數據。無論是何種類型的數據,都會以二進制序列的形式在網絡上傳送。發送方需要把這個Java對象轉換爲字節序列,才能在網絡上傳送;接收方則需要把字節序列再恢復爲Java對象。
===序列化的基本實現
只要對象實現了Serializable接口,對象的序列化就會變得十分簡單。要序列化一個對象首先要創建某些OutputStream對象,然後將其封裝在一個ObejctOutputStream對象內,這時只需要調用writeObject()即可將對象序列化,並將其發送給OutputStream。
對象序列化是基於字節的,所以要使用InputStream和OutputStream繼承層次結構
如果要反向上面的過程(即將一個序列還原爲一個對象),需要將一個InputStream封裝在ObjectInputStream內,然後調用readObject(),和往常一樣,我們最後獲得是一個引用,它指向了一個向上轉型的Object,所以必須向下轉型才能直接設置它們。
對象序列化不僅能夠將實現了接口的那個類進行序列化,也能夠將其引用的對象也實例化,以此類推。這種情況可以被稱之爲對象網。單個對象可與之建立連接。
我們舉個例子可以看到在序列化和反序列過程中,對象網中的連接的對象信息都沒有變。
public class TestSerializable {
public static void main(String[] args) throws IOException, ClassNotFoundException {
String fileName = "/Users/doc/test.sql";
Worm w = new Worm(6,'a');
System.out.println("w:"+w);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(fileName));
out.writeObject("Worm Storage\n");
out.writeObject(w);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream(fileName));
String s = (String) in.readObject();
Worm w2 = (Worm) in.readObject();
System.out.println(s+"w2:"+w2);
}
}
class Data implements Serializable{
private Integer i ;
public Data(Integer i ){
this.i = i;
}
@Override
public String toString() {
return i.toString();
}
}
class Worm implements Serializable{
private static final long serialVersionUID = 8033549288339500180L;
private static Random random = new Random(47);
private Data [] d = {
new Data(random.nextInt(10)),
new Data(random.nextInt(10)),
new Data(random.nextInt(10))
};
private Worm next;
private char c;
public Worm(int i ,char x){
System.out.println("Worm Constructor:"+i);
c = x;
if (--i>0){
next = new Worm(i,(char)(x+1));
}
}
public Worm(){
System.out.println("Default Constructor");
}
@Override
public String toString() {
StringBuffer result = new StringBuffer(":");
result.append(c);
result.append("(");
for (Data data: d){
result.append(data);
}
result.append(")");
if (next!=null){
result.append(next);
}
return result.toString();
}
}
可以看到打印信息如下
Worm Constructor:6
Worm Constructor:5
Worm Constructor:4
Worm Constructor:3
Worm Constructor:2
Worm Constructor:1
w::a(853):b(119):c(802):d(788):e(199):f(881)
Worm Storage
w2::a(853):b(119):c(802):d(788):e(199):f(881)
在生成Data對象時是用隨機數初始化的,從輸出中可以看出,被還原後的對象確實包含了原對象中的所有鏈接。
上面我們舉了個如何進行序列化的例子,其中或許看到了serialVersionUID 這個字段,如果不加的話,那麼系統會自動的生成一個,而如果修改了類的話,哪怕加一個空格那麼這個serialVersionUID 也會改變,那麼在反序列化的時候就會報錯,因爲在反序列化的時候會將serialVersionUID 和之前的serialVersionUID 進行對比,只有相同的時候纔會反序列化成功。所以還是建議顯視的定義一個serialVersionUID 。
transient(瞬時)關鍵字
當我們在對序列化進行控制的時候,可能需要某個字段不想讓Java進行序列化機制進行保存其信息與恢復。如果一個對象的字段保存了我們不希望將其序列化的敏感信息(例如密碼)。儘管我們使用private關鍵字但是如果經過序列化,那麼在進行反序列化的時候也是能將信息給恢復過來的。我們舉個例子如下:
我們定義個Student類
class Student implements Serializable{
private static final long serialVersionUID = 1734284264262085307L;
private String password;
------get set 方法
}
然後將其序列化到文件中然後再從文件中反序列化
public static void main(String[] args) throws IOException, ClassNotFoundException {
String fileName="/Users/doc/test.sql";
Student student = new Student();
student.setPassword("123456");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(fileName));
objectOutputStream.writeObject(student);
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(fileName));
Student readStudent = (Student) objectInputStream.readObject();
System.out.println(readStudent.getPassword());
}
然後發現輸出爲
readStudent的password=123456
此時我們如果想password參數在序列化的時候存儲其值,那麼可以加上transient關鍵字,就像下面一樣
private transient String password;
然後輸出如下
readStudent的password=null
發現在序列化的時候參數就已經沒被保存進去了
五:實現對象克隆的幾個工具類:
在做業務的時候,爲了隔離變化,我們會將DAO查詢出來的DO和對前端提供的DTO隔離開來。大概90%的時候,它們的結構都是類似的;但是我們很不喜歡寫很多冗長的b.setF1(a.getF1())這樣的代碼,於是我們需要簡化對象拷貝方式。
- BeanUtils(簡單,易用,Spring 包下的BeanUtil包更加穩定)
- BeanCopier(加入緩存後和手工set的性能接近)
- Dozer(深拷貝)
- fastjson(特定場景下使用)