שיטת clone() של אובייקט Java – שכפול ב-Java

שִׁכפּוּן הוא התהלכות שבה יוצרים העתק של אובייקט. מחלקת ה-Object של Java מגיעה עם שיטת ה-clone() המובנית שמחזירה את העתק של המופע הקיים. מאחר ו-Object הוא מחלקת הבסיס ב-Java, כל האובייקטים תומכים בברירת מחדל בפעולת השִׁכפּוּן.

שִׁכפּוּן אובייקט ב-Java

אם ברצונך להשתמש בשיטת ה-clone() של אובייקט ב-Java, עליך ליישם את ממשק הסימון java.lang.Cloneable. אחרת, התוכנית תזרוק את CloneNotSupportedException בזמן ריצה. גם ה-clone של ה-Object הוא שיטה מוגנת, ולכן עליך לדרוס אותה. בואו נצפה בשיטת השִׁכפּוּן של אובייקט ב-Java עם דוגמה.

package com.journaldev.cloning;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class Employee implements Cloneable {

	private int id;

	private String name;

	private Map<String, String> props;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Map<String, String> getProps() {
		return props;
	}

	public void setProps(Map<String, String> p) {
		this.props = p;
	}

	 @Override
	 public Object clone() throws CloneNotSupportedException {
	 return super.clone();
	 }

}

אנו משתמשים בשיטת ה-clone() של אובייקט, ולכן יישום את ממשק הסימון Cloneable מממשנים. אנו קוראים לשיטת ה-clone() של מערך המחלקות, כלומר שיטת ה-Object clone().

שימוש בשיטת Object clone()

בואו ליצור תוכנית בדיקה שתשתמש בשיטת ההעתקה clone() ליצירת עותק של המופע.

package com.journaldev.cloning;

import java.util.HashMap;
import java.util.Map;

public class CloningTest {

	public static void main(String[] args) throws CloneNotSupportedException {

		Employee emp = new Employee();

		emp.setId(1);
		emp.setName("Pankaj");
		Map props = new HashMap<>();
		props.put("salary", "10000");
		props.put("city", "Bangalore");
		emp.setProps(props);

		Employee clonedEmp = (Employee) emp.clone();

		// בדוק אם התכונות emp ו-clonedEmp הן זהות או שונות

		System.out.println("emp and clonedEmp == test: " + (emp == clonedEmp));
		
		System.out.println("emp and clonedEmp HashMap == test: " + (emp.getProps() == clonedEmp.getProps()));
		
		// בואו נראה את ההשפעה של שימוש בהעתקה ברירת-מחדל

		
		// שנה תכונות של emp

		emp.getProps().put("title", "CEO");
		emp.getProps().put("city", "New York");
		System.out.println("clonedEmp props:" + clonedEmp.getProps());

		// שנה את שם ה-emp

		emp.setName("new");
		System.out.println("clonedEmp name:" + clonedEmp.getName());

	}

}

פלט:

emp and clonedEmp == test: false
emp and clonedEmp HashMap == test: true
clonedEmp props:{city=New York, salary=10000, title=CEO}
clonedEmp name:Pankaj

CloneNotSupportedException בזמן ריצה

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

Exception in thread "main" java.lang.CloneNotSupportedException: com.journaldev.cloning.Employee
	at java.lang.Object.clone(Native Method)
	at com.journaldev.cloning.Employee.clone(Employee.java:41)
	at com.journaldev.cloning.CloningTest.main(CloningTest.java:19)

הבנת העתקת אובייקטים

בואו נסתכל על הפלט למעלה ונבין מה קורה עם שיטת ה-clone() של אובייקט.

  1. emp and clonedEmp == test: false: זה אומר ש-emp ו-clonedEmp הם שני אובייקטים שונים, ולא מפנים לאותו אובייקט. זה תואם לדרישות ההעתקה של אובייקטים ב-Java.
  2. emp ו-clonedEmp HashMap == בדיקה: נכון: לכן גם משתני האובייקט emp ו-clonedEmp מפנים לאותו אובייקט. זה יכול להיות בעיה רצינית של אינטגריטת נתונים אם נשנה את ערך האובייקט התחתון. כל שינוי בערך עשוי להישקף גם במופע המועתק.
  3. מאפייני clonedEmp:{עיר=ניו יורק, משכורת=10000, תפקיד=מנכ"ל}: לא עשינו שינוי במאפיינים של clonedEmp, אך עדיין הם השתנו מכיוון ששני המשתנים emp ו-clonedEmp מפנים לאותו אובייקט. המצב הזה מתרחש משום ששיטת העתקת Object הברירת מחדל יוצרת העתק שטחי. זה עשוי להיות בעיה כאשר רוצים ליצור אובייקטים מופרדים לחלוטין דרך תהליך ההעתקה. זה עשוי להביא לתוצאות לא רצויות, לכן חשוב לדרוס נכון את שיטת ההעתקה של Object.
  4. שם של clonedEmp:Pankaj: מה קרה כאן? שינינו את שם ה-emp אך שם ה-clonedEmp לא שונה. זה בגלל שהמחרוזת היא לא ניתנת לשינוי. לכן כאשר אנחנו מגדירים את שם ה-emp, מחרוזת חדשה נוצרת והפניה לשם ה-emp משתנה ב-this.name = name;. לכן שם ה-clonedEmp נשאר ללא שינוי. תמצאו התנהגות דומה גם לסוגי משתנים פרימיטיביים כלשהם. לכן אנחנו בסדר עם העתקת אובייקט ברירת מחדל של ג'אווה כל עוד יש לנו רק משתנים פרימיטיביים ולא ניתנים לשינוי באובייקט.

סוגי העתקות אובייקט

ישנם שני סוגים של שִׁקוּף אובייקט – שִׁקוּף פָּנִימִי וּשְׁקוּף עמוק. בואו להבין כל אחד מהם ונמצא את הדרך הטובה ביותר ליישם שִׁקוּף בתוכניות ה-Java שלנו.

1. שִׁקוּף פָּנִימִי

היישום ברירת המחדל של שיטת ה-Java Object clone() הוא באמצעות העתק שָׁטוּף. הוא משתמש ב-API השקפת תמונה ליצירת העתק של המופע. קטע הקוד למטה מציג דוגמא ליישום של שִׁקוּף פָּנִימִי.

@Override
 public Object clone() throws CloneNotSupportedException {
 
	 Employee e = new Employee();
	 e.setId(this.id);
	 e.setName(this.name);
	 e.setProps(this.props);
	 return e;
}

2. שִׁקוּף עמוק

בשִׁקוּף עמוק, אנו צריכים להעתיק שדות יחידה יחידה. אם יש לנו שדה עם אובייקטים מקוננים כמו List, Map וכו ', אז עלינו לכתוב את הקוד להעתקתם יחידה יחידה. זו הסיבה שנקרא לזה שִׁקוּף עמוק או העתק עמוק. אנו יכולים לדרוס את שיטת השִׁקוּף של העובד כמו בקוד הבא לשקוואף עמוק.

public Object clone() throws CloneNotSupportedException {

	Object obj = super.clone(); //utilize clone Object method

	Employee emp = (Employee) obj;

	// שִׁקוּף עמוק לשדות לא שניתן לשנות
	emp.setProps(null);
	Map hm = new HashMap<>();
	String key;
	Iterator it = this.props.keySet().iterator();
	// העתק עמוק של שדה לפי שדה
	while (it.hasNext()) {
		key = it.next();
		hm.put(key, this.props.get(key));
	}
	emp.setProps(hm);
	
	return emp;
}

עם יישום שיטת השִׁקוּף הזו, תוכנית הבדיקה שלנו תפיק את הפלט הבא.

emp and clonedEmp == test: false
emp and clonedEmp HashMap == test: false
clonedEmp props:{city=Bangalore, salary=10000}
clonedEmp name:Pankaj

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

שכפול באמצעות Serialization?

דרך אחת לבצע שכפול עמוק בקלות היא דרך serialization. אך serialization היא פעולה יקרה והמחלקה שלך צריכה לממש את הממשק Serializable. כל השדות והמחלקות האב צריכים גם לממש את Serializable.

שימוש ב־Apache Commons Util

אם כבר אתה משתמש במחלקות של Apache Commons Util בפרויקט שלך והמחלקה שלך היא סדרתית, אז תשתמש בשיטה הבאה.

Employee clonedEmp = org.apache.commons.lang3.SerializationUtils.clone(emp);

בנאי ההעתקה לשכפול

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

public Employee(Employee emp) {
	
	this.setId(emp.getId());
	this.setName(emp.getName());
	
	Map hm = new HashMap<>();
	String key;
	Iterator it = emp.getProps().keySet().iterator();
	// העתקה עמוקה שדה אחר שדה
	while (it.hasNext()) {
		key = it.next();
		hm.put(key, emp.getProps().get(key));
	}
	this.setProps(hm);

}

כל פעם שאנחנו זקוקים להעתקה של אובייקט עובד, אנו יכולים לקבל אותה באמצעות Employee clonedEmp = new Employee(emp);. אך כתיבת בנאי העתקה עשוי להיות משאבת מחקר אם המחלקה שלך מכילה הרבה משתנים, בעיקר פרימיטיביים ובלתי שינויים.

התרגלויות מומלצות לשכפול אובייקטים ב-Java

  1. השתמשו בשיטת ההעתקה ברירת המחדל של Object רק כאשר במחלקתך יש משתנים פרימיטיביים ובלתי שינויים או כאשר אתה רוצה העתק משטחי. במקרה של ירושה, עליכם לבדוק את כל המחלקות שאתם מרחיבים עד לרמת Object.

  2. ניתן גם להגדיר בנאי העתקה (Copy Constructor) אם למחלקתך רוב המאפיינים הם משתנים שניתנים לשינוי.

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

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

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

    @Override
    public Object clone() throws CloneNotSupportedException {
    
    	Object obj = super.clone();
    
    	Employee emp = (Employee) obj;
    
    	// שיכפול עמוק עבור שדות לא שינויים
    	emp.setProps(null);
    	Map<String, String> hm = new HashMap<>();
    	String key;
    	Iterator<String> it = this.props.keySet().iterator();
    	// שימור עמוק של שדה אחר שדה
    	while (it.hasNext()) {
    		key = it.next();
    		hm.put(key, this.props.get(key));
    	}
    	emp.setProps(hm);
    
    	return emp;
    }
    

    ניתן ליצור מחלקת ילד ולהשתמש בשיטת השכפול העמוק של מחלקת האב כך:

    package com.journaldev.cloning;
    
    public class EmployeeWrap extends Employee implements Cloneable {
    
    	private String title;
    
    	public String getTitle() {
    		return title;
    	}
    
    	public void setTitle(String t) {
    		this.title = t;
    	}
    
    	@Override
    	public Object clone() throws CloneNotSupportedException {
    
    		return super.clone();
    	}
    }
    

    המחלקה EmployeeWrap אין בה מאפיינים שניתנים לשינוי והיא משתמשת במימוש של שיטת השכפול של המחלקה האב. אם ישנם שדות שניתנים לשינוי, אז עליך לטפל בהעתקה עמוקה של אותם שדות בלבד. הנה תוכנית פשוטה לבדיקה האם אופן זה של שכפול עובד כמצופה או לא:

    package com.journaldev.cloning;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class CloningTest {
    
    	public static void main(String[] args) throws CloneNotSupportedException {
    
    		EmployeeWrap empWrap = new EmployeeWrap();
    
    		empWrap.setId(1);
    		empWrap.setName("Pankaj");
    		empWrap.setTitle("CEO");
    		
    		Map<String, String> props = new HashMap<>();
    		props.put("salary", "10000");
    		props.put("city", "Bangalore");
    		empWrap.setProps(props);
    
    		EmployeeWrap clonedEmpWrap = (EmployeeWrap) empWrap.clone();
    		
    		empWrap.getProps().put("1", "1");
    		
    		System.out.println("ערך מאפיין ניתן לשינוי ב- empWrap = "+empWrap.getProps());
    
    		System.out.println("ערך מאפיין ניתן לשינוי ב- clonedEmpWrap = "+clonedEmpWrap.getProps());
    		
    	}
    
    }
    

    פלט:

    ערך מאפיין ניתן לשינוי ב- empWrap = {1=1, city=Bangalore, salary=10000}
    ערך מאפיין ניתן לשינוי ב- clonedEmpWrap = {city=Bangalore, salary=10000}
    

    כך שעבד כמצופה.

זו כל הדברים שקשורים להעתקת אובייקטים ב-Java. אני מקווה שקיבלת רעיון אודות שימוש בשיטת clone() וכיצד לדרוס אותה בצורה נכונה ללא השפעות לוודאיות.

ניתן להוריד את הפרויקט מה-מאגר הקוד שלי ב-GitHub.

הפניה: מסמך ה- API עבור Object clone

Source:
https://www.digitalocean.com/community/tutorials/java-clone-object-cloning-java