Java 拷貝,你能說出個 123 麼?

2020-12-09 CSDN

作者 | sowhat1412 責編 | 張文

頭圖 | CSDN 下載自視覺中國

本文主要講解:對象創建方式、Java中的深拷貝和淺拷貝。

創建對象的5種方式

1.1 通過 new 關鍵字

這是最常用的一種方式,通過 new 關鍵字調用類的有參或無參構造方法來創建對象。比如 Object obj = new Object()。

1.2 通過 Class 類的 newInstance() 方法

這種默認是調用類的無參構造方法創建對象。比如:

Person p2 = (Person) Class. forName("com.ys.test. Person"). newInstance();

1.3 通過 Constructor 類的 newInstance 方法

這和第二種方法類時,都是通過反射來實現。通過 java.lang.relect.Constructor 類的 newInstance() 方法指定某個構造器來創建對象。實際上第二種方法利用 Class 的 newInstance() 方法創建對象,其內部調用還是 Constructor 的 newInstance() 方法。

Person p3 = (Person) Person.class.getConstructors()[0].newInstance();

1.4 利用 Clone 方法

Clone 是 Object 類中的一個方法,通過對象A.clone() 方法會創建一個內容和對象 A 一模一樣的對象 B,clone 克隆,顧名思義就是創建一個一模一樣的對象出來。  

Person p4 = (Person) p3.clone();

1.5 序列化

序列化是把堆內存中的 Java 對象數據,通過某種方式把對象存儲到磁碟文件中或者傳遞給其他網絡節點(在網絡上傳輸)。而反序列化則是把磁碟文件中的對象數據或者把網絡節點上的對象數據,恢復成Java對象模型的過程。

Java 基本複製方法

java賦值是複製對象引用,如果我們想要得到一個對象的==副本==,使用賦值操作是無法達到目的的:修改新對象的值會同時修改舊對象的值。

public class Client{ public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(15, "sowhat", new Address("河北", "建華南大街")); Person p1 = person; p1.setAge(45); System.out.println(p1.hashCode()); System.out.println(person.hashCode()); System.out.println("================"); System.out.println(p1.display()); System.out.println(person.display()); }}

Clone 方法

如果創建一個對象的新的副本,也就是說他們的初始狀態完全一樣,但以後可以改變各自的狀態,而互不影響,就需要用到java中對象的複製,如原生的clone()方法。本次講解的是 Java 的深拷貝和淺拷貝,其實現方式正是通過調用 Object 類的 clone() 方法來完成。在 Object.class 類中,源碼為:

/** * ... * performs a "shallow copy" of this object, not a "deep copy" operation. * 上面這裡已經說明了,clone()方法是淺拷貝,而不是深拷貝 * @see java.lang.Cloneable */ protected native Object clone() throws CloneNotSupportedException;

這是一個用 native 關鍵字修飾的方法。關於native關鍵字有一篇博客專門有介紹。不理解也沒關係,只需要知道用 native 修飾的方法就是告訴作業系統。這個方法我不實現了,讓作業系統去實現(參考JNI)。具體怎麼實現我們不需要了解,只需要知道clone方法的作用就是複製對象,產生一個新的對象。那麼這個新的對象和原對象是==什麼關係呢==?

基本類型和引用類型

這裡再給大家普及一個概念,在 Java 中基本類型和引用類型的區別。在 Java 中數據類型可以分為兩大類:基本類型和引用類型。

基本類型也稱為值類型,分別是字符類型 char布爾類型 boolean以及數值類型 byte、short、int、long、float、double

引用類型則包括類、接口、數組、枚舉等。

Java 將內存空間分為堆和棧。基本類型直接在棧 stack中存儲數值,而引用類型是將引用放在棧中,實際存儲的值是放在堆 heap中,通過棧中的引用指向堆中存放的數據。   

上圖定義的 a 和 b 都是基本類型,其值是直接存放在棧中的;而 c 和 d 是 String 聲明的,這是一個引用類型,引用地址是存放在棧中,然後指向堆的內存空間。下面 d = c,這條語句表示將 c 的引用賦值給 d,那麼 c 和 d 將指向同一塊堆內存空間。

淺拷貝

接下來用代碼看看淺拷貝的效果。

package mytest;@Data//lombok註解class Person implements Cloneable{ private int age; private String name; private Address address; public Person(int age, String name, Address address) { this.age = age; this.name = name; this.address = address; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public String display() { return "Person [age=" + age + ", name=" + name + ", address=" + address + "]"; }}@Data//lombok註解class Address{ private String province; private String street; public Address(String province, String street) { this.province = province; this.street = street; } @Override public String toString() { return "Address [province=" + province + ", street=" + street + "]"; }}public class Client{ public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(15, "sowhat", new Address("河北", "建華南大街")); Person clonePerson = (Person) person.clone(); System.out.println(person); System.out.println(clonePerson); // 信息完全一樣 System.out.println(person.display()); System.out.println(clonePerson.display()); System.out.println("信息完全一致"); System.out.println("原始年齡:" + person.getAge()); System.out.println("克隆後原始年齡:" + clonePerson.getAge()); System.out.println("年齡完全一樣"); System.out.println("原始名字哈希值:" + person.getName().hashCode()); System.out.println("克隆後名字哈希值:" + clonePerson.getName().hashCode()); System.out.println("字符串哈希值完全一樣"); clonePerson.setName("xiaomai"); clonePerson.setAge(20); clonePerson.getAddress().setStreet("中山路"); System.out.println(clonePerson.display()); System.out.println(person.display()); System.out.println("年齡跟姓名 是完全的深拷貝 副本跟原值無關的!"); System.out.println("地址信息的修改是淺拷貝 "); }}

結果如下:

mytest.Person@15f550a

mytest.Person@6b2d4a

Person [age=15, name=sowhat, address=Address [province=河北, street=建華南大街]]

Person [age=15, name=sowhat, address=Address [province=河北, street=建華南大街]]

信息完全一致

原始年齡:15

克隆後原始年齡:15

年齡完全一樣

原始名字哈希值:-1432601412

克隆後名字哈希值:-1432601412

字符串哈希值完全一樣

Person [age=20, name=xiaomai, address=Address [province=河北, street=中山路]]

Person [age=15, name=sowhat, address=Address [province=河北, street=中山路]]

結論:

原對象與新對象是兩個不同的對象。拷貝出來的新對象與原對象內容一致接著將新對象裡面的信息進行了修改,然後輸出發現原對象裡面的部分信息也跟著變了。其中基本類型跟 String類型的改變不會影響到原始對象的改變。而其他的Ojbect 類型改變的時候會影響到原始數據。上面的結論稱為淺拷貝。即創建一個新對象,然後將當前對象的非靜態欄位複製到該對象,如果欄位類型是值類型(基本類型跟String)的,那麼對該欄位進行複製;如果欄位是引用類型的則只複製該欄位的引用而不複製引用指向的對象(也就是只複製對象的地址)此時新對象裡面的引用類型欄位相當於是原始對象裡面引用類型欄位的一個副本,原始對象與新對象裡面的引用欄位指向的是同一個對象。因此,修改clonePerson裡面的address內容時,原person裡面的address內容會跟著改變。

深拷貝

了解了淺拷貝,那麼深拷貝是什麼也就很清楚了。那麼該如何實現深拷貝呢?Object 類提供的 clone 是只能實現淺拷貝的,即將引用類型的屬性內容也拷貝一份新的。

那麼,實現深拷貝我這裡收集到兩種方式:第一種是給需要拷貝的引用類型也實現Cloneable接口並覆寫clone方法;第二種則是利用序列化。

接下來分別對兩種方式進行演示:

深拷貝-clone方式

對於以上演示代碼,利用clone方式進行深拷貝無非就是將Address類也實現Cloneable,然後對Person的clone方法進行調整。讓每個引用類型屬性內部都重寫clone() 方法,既然引用類型不能實現深拷貝,那麼我們將每個引用類型都拆分為基本類型,分別進行淺拷貝。比如上面的例子,Person 類有一個引用類型 Address(其實String 也是引用類型,但是String類型有點特殊,後面會詳細講解),我們在 Address 類內部也重寫 clone 方法。如下:

package mytest;@Data//lombok註解class Person implements Cloneable{ private int age; private String name; private Address address; protected int abc = 12; public Person(int age, String name, Address address) { this.age = age; this.name = name; this.address = address; } @Override // clone 重載 protected Object clone() throws CloneNotSupportedException { Person person = (Person) super.clone(); //手動對address屬性進行clone,並賦值給新的person對象 person.address = (Address) address.clone(); return person; } public String display() { return "Person [age=" + age + ", name=" + name + ", address=" + address + "]"; }}@Data//lombok註解class Address implements Cloneable{ private String province; private String street; public Address(String province, String street) { this.province = province; this.street = street; } // 深拷貝時添加 @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } @Override public String toString() { return "Address [province=" + province + ", street=" + street + "]"; }}public class Client{ public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(15, "sowhat", new Address("河北", "建華南大街")); Person p1 = person; p1.setAge(45); System.out.println(p1.hashCode()); System.out.println(person.hashCode()); System.out.println(p1.display()); System.out.println(person.display()); System.out.println("-----------"); Person clonePerson = (Person) person.clone(); System.out.println(person); System.out.println(clonePerson); // 信息完全一樣 System.out.println(person.display()); System.out.println(clonePerson.display()); System.out.println("信息完全一致"); System.out.println("原始年齡:" + person.getAge()); System.out.println("克隆後原始年齡:" + clonePerson.getAge()); System.out.println("年齡完全一樣"); System.out.println("原始名字哈希值:" + person.getName().hashCode()); System.out.println("克隆後名字哈希值:" + clonePerson.getName().hashCode()); System.out.println("字符串哈希值完全一樣"); clonePerson.setName("sowhat1412"); clonePerson.setAge(20); clonePerson.getAddress().setStreet("中山路"); System.out.println(clonePerson.display()); System.out.println(person.display()); System.out.println("年齡跟姓名 是完全的深拷貝 副本跟原值無關的!"); System.out.println("地址信息的修改是淺拷貝 "); }}

但是這種做法有個弊端,這裡我們Person 類只有一個 Address 引用類型,而 Address 類沒有,所以我們只用重寫 Address 類的clone 方法。但是如果 Address 類也存在一個引用類型,那麼我們也要重寫其clone 方法,這樣下去,有多少個引用類型,我們就要重寫多少次,如果存在很多引用類型,那麼代碼量顯然會很大,所以這種方法不太合適。

利用序列化

序列化是將對象寫到流中便於傳輸,而反序列化則是把對象從流中讀取出來。這裡寫到流中的對象則是原始對象的一個拷貝,因為原始對象還存在 JVM 中,所以我們可以利用對象的序列化產生克隆對象,然後通過反序列化獲取這個對象。注意每個需要序列化的類都要實現 Serializable 接口,如果有某個屬性不需要序列化,可以將其聲明為 transient,即將其排除在克隆屬性之外。

package mytest;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.Serializable;/** * 利用序列化和反序列化進行對象的深拷貝 * @author ljj */classDeepCloneimplementsSerializable{privatestaticfinallong serialVersionUID = 1412L;public Object deepClone()throws Exception{//序列化 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this);//反序列化 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis);return ois.readObject(); }}@DataclassPersonextendsDeepClone{privatestaticfinallong serialVersionUID = 1L;privateint age;private String name;private Address address;publicPerson(int age, String name, Address address){this.age = age;this.name = name;this.address = address; }public String display(){return"Person [age=" + age + ", name=" + name + ", address=" + address + "]"; }}@DataclassAddressextendsDeepClone{privatestaticfinallong serialVersionUID = 1412L;private String province;private String street;publicAddress(String province, String street){this.province = province;this.street = street; }@Overridepublic String toString(){return"Address [province=" + province + ", street=" + street + "]"; }publicvoidsetStreet(String street){this.street = street; }}publicclassClient{publicstaticvoidmain(String[] args)throws Exception{ Person person = new Person(15, "sowhat", new Address("河北", "建華南大街")); Person clonePerson = (Person) person.deepClone(); System.out.println(person); System.out.println(clonePerson); System.out.println(person.display()); System.out.println(clonePerson.display()); clonePerson.setName("sowhat1412"); clonePerson.setAge(20); Address address = clonePerson.getAddress(); address.setStreet("中山路"); System.out.println(clonePerson.display()); System.out.println(person.display()); }}

相關焦點

  • 最早的家庭遊戲機小霸王——你能說出下面所有遊戲的名字麼?
    多個小動物幫助你上冰山,下火海,走沙漠,水底遊,天上飛!遊戲內容非常之豐富,難度也設計的很好,我打99分!這個版本我們習慣性的叫做XXX2代,實際它的名字是……我很好奇世界上接觸國遊戲的人中還有人不知道這個遊戲的麼?最早的,確是最經典的一款,就圖中這個關卡就難倒了多少人?我也是玩了好久後才知道可以按住B鍵去調整擊打白球的位置!
  • Java創建對象的幾種方式
    java是一種面向對象語言,所以我們在寫代碼過程中會創建很多對象,那java創建的對象到底有多少種呢?其中每種的差別又有哪些呢?請允許我慢慢道來1.使用new關鍵字這是最常見也是使用最多的一種。name);但有時候我們會遇到這種情況情況編譯提示錯誤,提示信息告訴我們,這個類的構造函數是private(私有的),外部無法使用,那麼此時也不要擔心,在我們設計這個類的時候,我們需要清楚的知道這個類的作用目的,以便我們更好的設計它的使用範圍,隱藏一個類new方式創建對象的時候,一定會提供其他方法給你創建對象
  • Java 面試如何坐等 offer?
    常言道「一屋不掃何以掃天下」,也是同樣的道理,如果連基礎的概念都搞不明白,又怎麼讓面試官相信你能寫出高質量的程序呢?與其浪費彼此的時間,還不如花點時間把自己的基礎知識掌握牢固。原因三:提高 Java 從業人員整體的能力模型,讓優秀的人能「冒」出來。
  • JAVA 基礎:JAVA開發環境搭建
    打開Path變量,在變量值最前加入 %JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;(方法同上)4.驗證:運行cmd,輸入java -version,顯示java版本則成功。下載jdk安裝包,使用wget命令或將安裝包ftp到伺服器上安裝目錄示例在/var/local下;3.進入 jdk1.8.0_181目錄下,輸入命令 pwd,獲取文件夾下的當前路徑;4.使用editplus 連接 伺服器將 下面的代碼拷貝到
  • Java 最常見的 200+ 面試題:面試必備
    就像之前聽過的一個故事,為什麼在美國有些企業只要看你是哈佛的學歷就直接錄取?並不是哈佛有多麼厲害,當然教學質量也是其中原因之一,但更多的是在美國上大學還是挺貴的,首先你能上的起哈佛,說明你的家庭條還不錯,從小應該就有很多參加更好教育的機會;第二,你能進入哈佛,也說明你腦子不笨,能考的上哈佛;最後才是哈佛確實能給你提供一個,相對不錯的教育環境。
  • 開發崗位這麼多,為什麼選Java?你學Java了嗎-開課吧
    零基礎學Java市場需求決定你的學習方向,招聘平臺上招程式設計師java佔比很高。以上是許多人選擇Java語言系統的重要原因。學習沒有捷徑,希望大家都能少走一些彎路,學有所成!Java語言相關內容推薦:java工程師工資一般多少?java自學容易嗎?
  • 你必須掌握的 21 個 Java 核心技術!
    而寫這篇文章的目的是想總結一下自己這麼多年來使用java的一些心得體會,希望可以給大家一些經驗,能讓大家更好學習和使用Java。這次介紹的主要內容是和J2SE相關的部分,另外,會在以後再介紹些J2EE相關的、和Java中各個框架相關的內容。
  • Java咖啡館(11):Java插件技術
    也就是說,只需要一個瀏覽器,它不必內置Java虛擬機(比如與Windows XP捆綁銷售的IE 6),也不必特意安裝Java運行環境,在打開包含Java Applet的網頁時,只要按照瀏覽器提示安裝這個Java插件後便能任意運行Applet了,而這個安裝過程與安裝Macromedia Flash、3721等插件一樣簡單。
  • Java程式設計師必備基礎:Java代碼是怎麼運行的?
    java 代碼運行主要流程 因此,在運行Java程序之前,需要編譯器把代碼編譯成java虛擬機所能識別的指令程序,這就是Java字節碼,即class文件。 所以,Java代碼運行的第一步是:把Java原始碼編譯成.class 字節碼文件。
  • 電腦小白:java和JavaScript啥關係?程式設計師:就像馬雲和馬如雲
    7、就像卡巴斯基和巴基斯坦一樣有基巴關係8、就像張三和張三丰的關係一樣9、就像周杰和周杰倫一樣10、就像菠蘿和菠蘿蜜的關係一樣估計也都是被問了無數遍的問題了,所以一下子就說出了這麼多的調侃段子而java則是一種程式語言,它是一種通過解釋方式來執行的語言。
  • Hstorage UHA-123DC硬碟拷貝機促銷
    (中關村在線北京行情)Hstorage UHA-123DC硬碟拷貝機速度快效率高,方便簡易的操作軟體,提供最完整之監控功能,支援數十種檔案格式進行快速複製資料,大幅減少複製時間增加產能。
  • 從Java中的FileInputStream讀取字節
    importjava.io.File;import java.io.FileInputStream;publicclass fileInputStream {publicstaticSystem.out.println("read " + readBytes + " bytes, and placed them into temp array named data");System.out.println("data :" + data[123]);
  • 新手轉行學java難嗎?新手學java需要注意的6個方面!
    新手轉行學java難嗎?新手學java需要注意的6個方面!所以,我們得出結論,能系統學習的,有老師指導的java課程學起來相對容易,而沒有人指導,僅僅靠看視頻學習的同學來說會無形中增加難度。很多新手在準備轉行學習java之前,在網上看到或聽到很多不懂的人會說,java有多麼困難,普通人還是不要去學習,所以也會有人在初次了解的階段就放棄了繼續深入了解的機會。但是也有堅持下來的同學,最後努力堅持下來,並找到一份高薪的工作。
  • java工程師工資一般多少?java自學容易嗎?公司會要嗎-開課吧
    java自學容易,自學後找到工作也不算難,但是想要摸到這個行業的天花板就很難了!多多交流溝通,其他人自學中走過的路對你而言具有非常寶貴的借鑑異議。還能讓你少走不少彎路!學習沒有捷徑,希望大家都能少走一些彎路,在學習Java的道路上一往無前,學有所成!java語言相關內容推薦:
  • 程式設計師學Java要關注的6個網站,你知道幾個呢?
    下面w3cschool給程式設計師小夥伴們分享java學習的6個網站:0、SourgeForgeSourgeForge是開源軟體開發者進行開發管理的集中式網站。有相當豐富的Java開放原始碼的著名的軟體。1、w3cschool網站有不少入門Java的程式設計師學了幾個月一頭霧水,抓不住一些重點、核心的編程知識點。
  • 給Java新手的一些建議——Java知識點歸納(Java基礎部分)
    寫這篇文章的目的是想總結一下自己這麼多年來使用java的一些心得體會,主要是和一些java基礎知識點相關的,所以也希望能分享給剛剛入門的Java程式設計師和打算入Java開發這個行當的準新手們,希望可以給大家一些經驗,能讓大家更好學習和使用Java。這次介紹的主要內容是和J2SE相關的部分,另外,會在以後再介紹些J2EE相關的、和Java中各個框架相關的內容。
  • EffectiveJava-7-通用程序設計
    有專人或組織維護,性能隨著版本的迭代會不斷提高,bug也會被逐漸發現並修復,功能也會不斷豐富;所以說不管是java還是Android,及時關注官方的api更新說明還是很有必要的,因為如果你不知道這些類庫或者新增加的功能, 可能會花費百倍千倍的時間去寫一些沒有必要,甚至可能有bug的的代碼,總之不要重複造輪子;如果需要精確的答案,請避免使用float和double
  • 新樂塵符123我愛你在哪試聽 123我愛你完整版歌詞歌曲介紹
    打開愛情手冊,大聲說出《123我愛你》。這是我們的專屬情歌。陪你看日落,陪你等雨過;陪你一起過所有甜蜜的節日!  濃濃的聖誕節氣氛下,攜著愛人的手一起度過,聽著新樂塵符的這首最新單曲,在寒冬下向心裡的ta訴說的愛意吧!
  • Java基礎學習:一篇文章讓你搞懂Java字符串的前世今生
    你要知道,首先他們的大小不一樣,其次上面的 char[] 中的 97(a),98(b),99(c) 都屬於拉丁字符集,如果用到其它字符集,那麼結果就不一樣了,看下面的例子例1,按 gbk 字符集轉換newString(newbyte[]{(byte)0xD5,(byte)0xC5}, Charset.forName