כיצד ליצור העתק עמוק של אובייקט בג'אווה

1. הקדמה

כשאנחנו רוצים להעתיק אובייקט ב- Java, יש שתי אפשרויות שעלינו לקחת בחשבון - עותק רדוד והעתק עמוק.

העותק הרדוד הוא הגישה כאשר אנו מעתיקים רק ערכי שדה ולכן העותק עשוי להיות תלוי באובייקט המקורי. בגישת ההעתקה העמוקה, אנו מוודאים כי כל האובייקטים בעץ מועתקים עמוק, כך שההעתק אינו תלוי באובייקט קיים קודם אשר עשוי להשתנות אי פעם.

במאמר זה נשווה בין שתי הגישות הללו ונלמד ארבע שיטות ליישום העותק העמוק.

2. הגדרת Maven

נשתמש בשלוש תלות של Maven - Gson, Jackson ו- Apache Commons Lang - כדי לבדוק דרכים שונות לביצוע עותק עמוק.

בואו נוסיף את התלות הללו שלנו pom.xml:

 com.google.code.gson gson 2.8.2 commons-lang commons-lang 2.6 com.fasterxml.jackson.core jackson-databind 2.9.3 

הגרסאות האחרונות של Gson, Jackson ו- Apache Commons Lang נמצאות ב- Maven Central.

3. מודל

כדי להשוות שיטות שונות להעתקת אובייקטים של Java, נצטרך לעבוד על שתי כיתות:

כתובת כתובת {רחוב מיתרים פרטי; עיר מיתרים פרטית; מדינה מחרוזת פרטית; // קונסטרוקציות סטנדרטיות, קבוצות וקובעות}
מחלקה משתמש {שם פרטי פרטי מחרוזת; שם משפחה פרטי מחרוזת; כתובת כתובת פרטית; // קונסטרוקציות סטנדרטיות, קבוצות וקובעות}

4. העתקה רדודה

עותק רדוד הוא אחד בו אנו מעתיקים רק ערכים של שדות מאובייקט אחד לאחר:

@ מבחן ציבורי בטל כאשר ShallowCopying_thenObjectsShouldNotBeSame () {כתובת כתובת = כתובת חדשה ("רחוב Downing 10", "לונדון", "אנגליה"); משתמש pm = משתמש חדש ("ראש", "שר", כתובת); משתמש shallowCopy = משתמש חדש (pm.getFirstName (), pm.getLastName (), pm.getAddress ()); assertThat (shallowCopy) .isNotSameAs (pm); }

במקרה הזה pm! = העתק רדוד, מה שאומר ש הם אובייקטים שונים, אך הבעיה היא שכאשר אנו משנים כל אחד מהמקור כתובת' מאפיינים, זה ישפיע גם על רדוד העתקהכתובת.

לא היינו טורחים בעניין אם כתובת היה בלתי משתנה, אבל זה לא:

@ מבחן ציבורי בטל כאשרModifyingOriginalObject_ThenCopyShouldChange () {כתובת = כתובת חדשה ("Downing St 10", "לונדון", "אנגליה"); משתמש pm = משתמש חדש ("ראש", "שר", כתובת); משתמש shallowCopy = משתמש חדש (pm.getFirstName (), pm.getLastName (), pm.getAddress ()); address.setCountry ("בריטניה הגדולה"); assertThat (shallowCopy.getAddress (). getCountry ()) .isEqualTo (pm.getAddress (). getCountry ()); }

5. העתק עמוק

העתק עמוק הוא חלופה הפותרת בעיה זו. היתרון שלה הוא שלפחות כל אובייקט משתנה בגרף האובייקט מועתק רקורסיבית.

מכיוון שהעותק אינו תלוי באובייקט משתנה שנוצר קודם לכן, הוא לא ישתנה בטעות כמו שראינו עם העותק הרדוד.

בחלקים הבאים נציג כמה יישומי עותקים עמוקים ונדגים יתרון זה.

5.1. בונה העתקות

היישום הראשון שניישם מבוסס על בוני העתקים:

כתובת ציבורית (כתובת זה) {this (that.getStreet (), that.getCity (), that.getCountry ()); }
משתמש ציבורי (משתמש ש) {זה (that.getFirstName (), that.getLastName (), כתובת חדשה (that.getAddress ())); }

ביישום לעיל של העותק העמוק, לא יצרנו חדש מיתרים בבנאי ההעתקים שלנו בגלל חוּט הוא מעמד שאינו משתנה.

כתוצאה מכך, לא ניתן לשנותם במקרה. בואו נראה אם ​​זה עובד:

@ מבחן ציבורי בטל כאשרModifyingOriginalObject_thenCopyShouldNotChange () {כתובת כתובת = כתובת חדשה ("Downing St 10", "London", "England"); משתמש pm = משתמש חדש ("ראש", "שר", כתובת); משתמש deepCopy = משתמש חדש (pm); address.setCountry ("בריטניה הגדולה"); assertNotEquals (pm.getAddress (). getCountry (), deepCopy.getAddress (). getCountry ()); }

5.2. ממשק משובט

היישום השני מבוסס על שיטת השיבוט שעברה בירושה לְהִתְנַגֵד. זה מוגן, אבל אנחנו צריכים לבטל את זה כ פּוּמְבֵּי.

נוסיף גם ממשק סמן, ניתנת לניבוב לשיעורים כדי לציין שהשיעורים למעשה ניתנים לשיבוץ.

בואו נוסיף את שיבוט () שיטה ל כתובת מעמד:

@ Override שיבוט אובייקט ציבורי () {נסה {return (כתובת) super.clone (); } לתפוס (CloneNotSupportedException e) {להחזיר כתובת חדשה (this.street, this.getCity (), this.getCountry ()); }}

ועכשיו בואו ניישם שיבוט () בשביל ה מִשׁתַמֵשׁ מעמד:

@ Override שיבוט אובייקט ציבורי () {משתמש משתמש = null; נסה {user = (User) super.clone (); } לתפוס (CloneNotSupportedException e) {user = משתמש חדש (this.getFirstName (), this.getLastName (), this.getAddress ()); } user.address = (כתובת) this.address.clone (); משתמש חוזר; }

שים לב שה- super.clone () שיחה מחזירה עותק רדוד של אובייקט, אך אנו קובעים עותקים עמוקים של שדות ניתנים לשינוי באופן ידני, כך שהתוצאה נכונה:

@ מבחן ציבורי בטל כאשרModifyingOriginalObject_thenCloneCopyShouldNotChange () {כתובת כתובת = כתובת חדשה ("Downing St 10", "London", "England"); משתמש pm = משתמש חדש ("ראש", "שר", כתובת); משתמש deepCopy = (משתמש) pm.clone (); address.setCountry ("בריטניה הגדולה"); assertThat (deepCopy.getAddress (). getCountry ()) .isNotEqualTo (pm.getAddress (). getCountry ()); }

6. ספריות חיצוניות

הדוגמאות הנ"ל נראות קלות, אך לעיתים אינן חלות כפתרון כאשר איננו יכולים להוסיף קונסטרוקטור נוסף או לעקוף את שיטת השיבוט.

זה עשוי לקרות כאשר איננו הבעלים של הקוד, או כאשר גרף האובייקטים כל כך מסובך שלא נסיים את הפרויקט שלנו בזמן אם נתמקד בכתיבת בונים נוספים או ביישום הפרויקט. שיבוט שיטה בכל הכיתות בגרף האובייקט.

מה אז? במקרה זה נוכל להשתמש בספריה חיצונית. כדי להשיג עותק עמוק, אנחנו יכולים לסדר אובייקט ואז לערוך אותו מחדש לאובייקט חדש.

בואו נסתכל על כמה דוגמאות.

6.1. אפאצ'י קומונס לאנג

לאפאצ'י קומונס לאנג SerializationUtils # שיבוט, שמבצע העתק עמוק כאשר כל המחלקות בגרף האובייקט מיישמות את ניתן לבצע סדרתי מִמְשָׁק.

אם השיטה נתקלת בכיתה שאינה ניתנת לסידור, היא תיכשל ותשליך לא מסומן SerializationException.

בגלל זה, עלינו להוסיף את ניתן לבצע סדרתי ממשק לשיעורים שלנו:

@ מבחן ציבורי בטל כאשרModifyingOriginalObject_thenCommonsCloneShouldNotChange () {כתובת כתובת = כתובת חדשה ("רחוב Downing 10", "לונדון", "אנגליה"); משתמש pm = משתמש חדש ("ראש", "שר", כתובת); משתמש deepCopy = (משתמש) SerializationUtils.clone (pm); address.setCountry ("בריטניה הגדולה"); assertThat (deepCopy.getAddress (). getCountry ()) .isNotEqualTo (pm.getAddress (). getCountry ()); }

6.2. JSON סידור עם Gson

הדרך השנייה לבצע סדרת סדרות היא להשתמש בסידור JSON. Gson היא ספרייה המשמשת להמרת אובייקטים ל- JSON ולהיפך.

שלא כמו אפאצ'י קומונס לאנג, GSON אינו זקוק ל ניתן לבצע סדרתי ממשק לביצוע ההמרות.

בואו נסתכל במהירות על דוגמה:

@ מבחן ציבורי בטל כאשרModifyingOriginalObject_thenGsonCloneShouldNotChange () {כתובת כתובת = כתובת חדשה ("Downing St 10", "London", "England"); משתמש pm = משתמש חדש ("ראש", "שר", כתובת); Gson gson = Gson חדש (); משתמש deepCopy = gson.fromJson (gson.toJson (pm), User.class); address.setCountry ("בריטניה הגדולה"); assertThat (deepCopy.getAddress (). getCountry ()) .isNotEqualTo (pm.getAddress (). getCountry ()); }

6.3. JSON סידור עם ג'קסון

ג'קסון היא ספרייה נוספת התומכת בסידור JSON. יישום זה יהיה דומה מאוד לזה שמשתמש ב- Gson, אך עלינו להוסיף את קונסטרוקטור ברירת המחדל לשיעורים שלנו.

בואו נראה דוגמה:

@ מבחן ציבורי בטל כאשרModifyingOriginalObject_thenJacksonCopyShouldNotChange () זורק IOException {כתובת כתובת = כתובת חדשה ("רחוב Downing 10", "לונדון", "אנגליה"); משתמש pm = משתמש חדש ("ראש", "שר", כתובת); ObjectMapper objectMapper = ObjectMapper חדש (); משתמש deepCopy = objectMapper .readValue (objectMapper.writeValueAsString (pm), User.class); address.setCountry ("בריטניה הגדולה"); assertThat (deepCopy.getAddress (). getCountry ()) .isNotEqualTo (pm.getAddress (). getCountry ()); }

7. מסקנה

באיזה יישום עלינו להשתמש בעת יצירת עותק עמוק? ההחלטה הסופית תלויה לרוב בשיעורים שנעתיק והאם בבעלותנו המחלקות בגרף האובייקט.

כמו תמיד, ניתן למצוא את דגימות הקוד המלאות עבור הדרכה זו באתר GitHub.