日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

java 复制对象_Java程序员必备:序列化全方位解析

發布時間:2023/12/10 java 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java 复制对象_Java程序员必备:序列化全方位解析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言
相信大家日常開發中,經常看到Java對象“implements Serializable”。那么,它到底有什么用呢?本文從以下幾個角度來解析序列這一塊知識點~

  • 什么是Java序列化?
  • 為什么需要序列化?
  • 序列化用途
  • Java序列化常用API
  • 序列化的使用
  • 序列化底層
  • 日常開發序列化的注意點
  • 序列化常見面試題

一、什么是Java序列化?

  • 序列化:把Java對象轉換為字節序列的過程
  • 反序列:把字節序列恢復為Java對象的過程

二、為什么需要序列化?
Java對象是運行在JVM的堆內存中的,如果JVM停止后,它的生命也就戛然而止。

如果想在JVM停止后,把這些對象保存到磁盤或者通過網絡傳輸到另一遠程機器,怎么辦呢?磁盤這些硬件可不認識Java對象,它們只認識二進制這些機器語言,所以我們就要把這些對象轉化為字節數組,這個過程就是序列化啦~
打個比喻,作為大城市漂泊的碼農,搬家是常態。當我們搬書桌時,桌子太大了就通不過比較小的門,因此我們需要把它拆開再搬過去,這個拆桌子的過程就是序列化。 而我們把書桌復原回來(安裝)的過程就是反序列化啦。
三、序列化用途
序列化使得對象可以脫離程序運行而獨立存在,它主要有兩種用途:

  • 1) 序列化機制可以讓對象地保存到硬盤上,減輕內存壓力的同時,也起了持久化的作用;

比如 Web服務器中的Session對象,當有 10+萬用戶并發訪問的,就有可能出現10萬個Session對象,內存可能消化不良,于是Web容器就會把一些seesion先序列化到硬盤中,等要用了,再把保存在硬盤中的對象還原到內存中。

  • 2) 序列化機制讓Java對象在網絡傳輸不再是天方夜譚。

我們在使用Dubbo遠程調用服務框架時,需要把傳輸的Java對象實現Serializable接口,即讓Java對象序列化,因為這樣才能讓對象在網絡上傳輸。
四、Java序列化常用API
java.io.ObjectOutputStream java.io.ObjectInputStream java.io.Serializable java.io.Externalizable 復制代碼
Serializable 接口
Serializable接口是一個標記接口,沒有方法或字段。一旦實現了此接口,就標志該類的對象就是可序列化的。
public interface Serializable { } 復制代碼
Externalizable 接口
Externalizable繼承了Serializable接口,還定義了兩個抽象方法:writeExternal()和readExternal(),如果開發人員使用Externalizable來實現序列化和反序列化,需要重寫writeExternal()和readExternal()方法
public interface Externalizable extends java.io.Serializable { void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; } 復制代碼
java.io.ObjectOutputStream類
表示對象輸出流,它的writeObject(Object obj)方法可以對指定obj對象參數進行序列化,再把得到的字節序列寫到一個目標輸出流中。
java.io.ObjectInputStream
表示對象輸入流, 它的readObject()方法,從輸入流中讀取到字節序列,反序列化成為一個對象,最后將其返回。
五、序列化的使用
序列化如何使用?來看一下,序列化的使用的幾個關鍵點吧:

  • 聲明一個實體類,實現Serializable接口
  • 使用ObjectOutputStream類的writeObject方法,實現序列化
  • 使用ObjectInputStream類的readObject方法,實現反序列化

聲明一個Student類,實現Serializable
public class Student implements Serializable { private Integer age; private String name; public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } } 復制代碼
使用ObjectOutputStream類的writeObject方法,對Student對象實現序列化
把Student對象設置值后,寫入一個文件,即序列化,哈哈~
ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("D:text.out")); Student student = new Student(); student.setAge(25); student.setName("jayWei"); objectOutputStream.writeObject(student); objectOutputStream.flush(); objectOutputStream.close(); 復制代碼
看看序列化的可愛模樣吧,test.out文件內容如下(使用UltraEdit打開):


使用ObjectInputStream類的readObject方法,實現反序列化,重新生成student對象
再把test.out文件讀取出來,反序列化為Student對象
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:text.out")); Student student = (Student) objectInputStream.readObject(); System.out.println("name="+student.getName()); 復制代碼


六、序列化底層
Serializable底層
Serializable接口,只是一個空的接口,沒有方法或字段,為什么這么神奇,實現了它就可以讓對象序列化了?
public interface Serializable { } 復制代碼
為了驗證Serializable的作用,把以上demo的Student對象,去掉實現Serializable接口,看序列化過程怎樣吧~


序列化過程中拋出異常啦,堆棧信息如下:
Exception in thread "main" java.io.NotSerializableException: com.example.demo.Student at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184) at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348) at com.example.demo.Test.main(Test.java:13) 復制代碼
順著堆棧信息看一下,原來有重大發現,如下~

原來底層是這樣: ObjectOutputStream 在序列化的時候,會判斷被序列化的Object是哪一種類型,String?array?enum?還是 Serializable,如果都不是的話,拋出 NotSerializableException異常。所以呀,Serializable真的只是一個標志,一個序列化標志~
writeObject(Object)
序列化的方法就是writeObject,基于以上的demo,我們來分析一波它的核心方法調用鏈吧~(建議大家也去debug看一下這個方法,感興趣的話)


writeObject直接調用的就是writeObject0()方法,
public final void writeObject(Object obj) throws IOException { ...... writeObject0(obj, false); ...... } 復制代碼
writeObject0 主要實現是對象的不同類型,調用不同的方法寫入序列化數據,這里面如果對象實現了Serializable接口,就調用writeOrdinaryObject()方法~
private void writeObject0(Object obj, boolean unshared) throws IOException { ...... //String類型 if (obj instanceof String) { writeString((String) obj, unshared); //數組類型 } else if (cl.isArray()) { writeArray(obj, desc, unshared); //枚舉類型 } else if (obj instanceof Enum) { writeEnum((Enum<?>) obj, desc, unshared); //Serializable實現序列化接口 } else if (obj instanceof Serializable) { writeOrdinaryObject(obj, desc, unshared); } else{ //其他情況會拋異常~ if (extendedDebugInfo) { throw new NotSerializableException( cl.getName() + "n" + debugInfoStack.toString()); } else { throw new NotSerializableException(cl.getName()); } } ...... 復制代碼
writeOrdinaryObject()會先調用writeClassDesc(desc),寫入該類的生成信息,然后調用writeSerialData方法,寫入序列化數據
private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) throws IOException { ...... //調用ObjectStreamClass的寫入方法 writeClassDesc(desc, false); // 判斷是否實現了Externalizable接口 if (desc.isExternalizable() && !desc.isProxy()) { writeExternalData((Externalizable) obj); } else { //寫入序列化數據 writeSerialData(obj, desc); } ..... } 復制代碼
writeSerialData()實現的就是寫入被序列化對象的字段數據
private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException { for (int i = 0; i < slots.length; i++) { if (slotDesc.hasWriteObjectMethod()) { //如果被序列化的對象自定義實現了writeObject()方法,則執行這個代碼塊 slotDesc.invokeWriteObject(obj, this); } else { // 調用默認的方法寫入實例數據 defaultWriteFields(obj, slotDesc); } } } 復制代碼
defaultWriteFields()方法,獲取類的基本數據類型數據,直接寫入底層字節容器;獲取類的obj類型數據,循環遞歸調用writeObject0()方法,寫入數據~
private void defaultWriteFields(Object obj, ObjectStreamClass desc) throws IOException { // 獲取類的基本數據類型數據,保存到primVals字節數組 desc.getPrimFieldValues(obj, primVals); //primVals的基本類型數據寫到底層字節容器 bout.write(primVals, 0, primDataSize, false); // 獲取對應類的所有字段對象 ObjectStreamField[] fields = desc.getFields(false); Object[] objVals = new Object[desc.getNumObjFields()]; int numPrimFields = fields.length - objVals.length; // 獲取類的obj類型數據,保存到objVals字節數組 desc.getObjFieldValues(obj, objVals); //對所有Object類型的字段,循環 for (int i = 0; i < objVals.length; i++) { ...... //遞歸調用writeObject0()方法,寫入對應的數據 writeObject0(objVals[i], fields[numPrimFields + i].isUnshared()); ...... } } 復制代碼
七、日常開發序列化的一些注意點

  • static靜態變量和transient 修飾的字段是不會被序列化的
  • serialVersionUID問題
  • 如果某個序列化類的成員變量是對象類型,則該對象類型的類必須實現序列化
  • 子類實現了序列化,父類沒有實現序列化,父類中的字段丟失問題

static靜態變量和transient 修飾的字段是不會被序列化的
static靜態變量和transient 修飾的字段是不會被序列化的,我們來看例子分析一波~ Student類加了一個類變量gender和一個transient修飾的字段specialty
public class Student implements Serializable { private Integer age; private String name; public static String gender = "男"; transient String specialty = "計算機專業"; public String getSpecialty() { return specialty; } public void setSpecialty(String specialty) { this.specialty = specialty; } @Override public String toString() { return "Student{" +"age=" + age + ", name='" + name + ''' + ", gender='" + gender + ''' + ", specialty='" + specialty + ''' + '}'; } ...... 復制代碼
打印學生對象,序列化到文件,接著修改靜態變量的值,再反序列化,輸出反序列化后的對象~

運行結果:
序列化前Student{age=25, name='jayWei', gender='男', specialty='計算機專業'} 序列化后Student{age=25, name='jayWei', gender='女', specialty='null'} 復制代碼
對比結果可以發現:

  • 1)序列化前的靜態變量性別明明是‘男’,序列化后再在程序中修改,反序列化后卻變成‘女’了,what?顯然這個靜態屬性并沒有進行序列化。其實,靜態(static)成員變量是屬于類級別的,而序列化是針對對象的~所以不能序列化哦。
  • 2)經過序列化和反序列化過程后,specialty字段變量值由'計算機專業'變為空了,為什么呢?其實是因為transient關鍵字,它可以阻止修飾的字段被序列化到文件中,在被反序列化后,transient 字段的值被設為初始值,比如int型的值會被設置為 0,對象型初始值會被設置為null。

serialVersionUID問題
serialVersionUID 表面意思就是序列化版本號ID,其實每一個實現Serializable接口的類,都有一個表示序列化版本標識符的靜態變量,或者默認等于1L,或者等于對象的哈希碼。
private static final long serialVersionUID = -6384871967268653799L; 復制代碼serialVersionUID有什么用?
JAVA序列化的機制是通過判斷類的serialVersionUID來驗證版本是否一致的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID和本地相應實體類的serialVersionUID進行比較,如果相同,反序列化成功,如果不相同,就拋出InvalidClassException異常。
接下來,我們來驗證一下吧,修改一下Student類,再反序列化操作


Exception in thread "main" java.io.InvalidClassException: com.example.demo.Student; local class incompatible: stream classdesc serialVersionUID = 3096644667492403394, local class serialVersionUID = 4429793331949928814 at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687) at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1876) at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1745) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2033) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1567) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:427) at com.example.demo.Test.main(Test.java:20) 復制代碼
從日志堆棧異常信息可以看到,文件流中的class和當前類路徑中的class不同了,它們的serialVersionUID不相同,所以反序列化拋出InvalidClassException異常。那么,如果確實需要修改Student類,又想反序列化成功,怎么辦呢?可以手動指定serialVersionUID的值,一般可以設置為1L或者,或者讓我們的編輯器IDE生成
private static final long serialVersionUID = -6564022808907262054L; 復制代碼
實際上,阿里開發手冊,強制要求序列化類新增屬性時,不能修改serialVersionUID字段~


如果某個序列化類的成員變量是對象類型,則該對象類型的類必須實現序列化
給Student類添加一個Teacher類型的成員變量,其中Teacher是沒有實現序列化接口的
public class Student implements Serializable { private Integer age; private String name; private Teacher teacher; ... } //Teacher 沒有實現 public class Teacher { ...... } 復制代碼
序列化運行,就報NotSerializableException異常啦
Exception in thread "main" java.io.NotSerializableException: com.example.demo.Teacher at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184) at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548) at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509) at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432) at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178) at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348) at com.example.demo.Test.main(Test.java:16) 復制代碼
其實這個可以在上小節的底層源碼分析找到答案,一個對象序列化過程,會循環調用它的Object類型字段,遞歸調用序列化的,也就是說,序列化Student類的時候,會對Teacher類進行序列化,但是對Teacher沒有實現序列化接口,因此拋出NotSerializableException異常。所以如果某個實例化類的成員變量是對象類型,則該對象類型的類必須實現序列化


子類實現了Serializable,父類沒有實現Serializable接口的話,父類不會被序列化。
子類Student實現了Serializable接口,父類User沒有實現Serializable接口
//父類實現了Serializable接口 public class Student extends User implements Serializable { private Integer age; private String name; } //父類沒有實現Serializable接口 public class User { String userId; } Student student = new Student(); student.setAge(25); student.setName("jayWei"); student.setUserId("1"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:text.out")); objectOutputStream.writeObject(student); objectOutputStream.flush(); objectOutputStream.close(); //反序列化結果 ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:text.out")); Student student1 = (Student) objectInputStream.readObject(); System.out.println(student1.getUserId()); //output /** * null */ 復制代碼
從反序列化結果,可以發現,父類屬性值丟失了。因此子類實現了Serializable接口,父類沒有實現Serializable接口的話,父類不會被序列化。
八、序列化常見面試題

  • 序列化的底層是怎么實現的?
  • 序列化時,如何讓某些成員不要序列化?
  • 在 Java 中,Serializable 和 Externalizable 有什么區別
  • serialVersionUID有什么用?
  • 是否可以自定義序列化過程, 或者是否可以覆蓋 Java 中的默認序列化過程?
  • 在 Java 序列化期間,哪些變量未序列化?

1.序列化的底層是怎么實現的?
本文第六小節可以回答這個問題,如回答Serializable關鍵字作用,序列化標志啦,源碼中,它的作用啦~還有,可以回答writeObject幾個核心方法,如直接寫入基本類型,獲取obj類型數據,循環遞歸寫入,哈哈~
2.序列化時,如何讓某些成員不要序列化?
可以用transient關鍵字修飾,它可以阻止修飾的字段被序列化到文件中,在被反序列化后,transient 字段的值被設為初始值,比如int型的值會被設置為 0,對象型初始值會被設置為null。
3.在 Java 中,Serializable 和 Externalizable 有什么區別
Externalizable繼承了Serializable,給我們提供 writeExternal() 和 readExternal() 方法, 讓我們可以控制 Java的序列化機制, 不依賴于Java的默認序列化。正確實現 Externalizable 接口可以顯著提高應用程序的性能。
4.serialVersionUID有什么用?
可以看回本文第七小節哈,JAVA序列化的機制是通過判斷類的serialVersionUID來驗證版本是否一致的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID和本地相應實體類的serialVersionUID進行比較,如果相同,反序列化成功,如果不相同,就拋出InvalidClassException異常。
5.是否可以自定義序列化過程, 或者是否可以覆蓋 Java 中的默認序列化過程?
可以的。我們都知道,對于序列化一個對象需調用 ObjectOutputStream.writeObject(saveThisObject), 并用 ObjectInputStream.readObject() 讀取對象, 但 Java 虛擬機為你提供的還有一件事, 是定義這兩個方法。如果在類中定義這兩種方法, 則 JVM 將調用這兩種方法, 而不是應用默認序列化機制。同時,可以聲明這些方法為私有方法,以避免被繼承、重寫或重載。
6.在 Java 序列化期間,哪些變量未序列化?
static靜態變量和transient 修飾的字段是不會被序列化的。靜態(static)成員變量是屬于類級別的,而序列化是針對對象的。transient關鍵字修字段飾,可以阻止該字段被序列化到文件中。

總結

以上是生活随笔為你收集整理的java 复制对象_Java程序员必备:序列化全方位解析的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。