איך ליצור כיתה לא ניתנת לשינויים ב-Java

הקדמה

מאמר זה מספק סקירה כיצד ליצור מחלקה ללא תוקף בתכנות ב-Java.

אובייקט הוא לא ניתן לשינוי כאשר המצב שלו אינו משתנה לאחר שהוא אובייקט שנאתחל. לדוגמה, String הוא מחלקה לא ניתנת לשינוי ולאחר השהייה, ערך של אובייקט מסוג String אינו משתנה. למד עוד על למה מחלקת ה-String היא לא ניתנת לשינוי ב-Java.

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

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

למד עוד על מרובי-ליבה ב-Java וסייר ב שאלות ראיון על מרובי-ליבה ב-Java.

יצירת מחלקה ללא אפשרות שינוי (Immutable) ב-Java

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

  1. הגדר את המחלקה כ-final כך שלא תוכל להיות מורשת.
  2. הגדר את כל השדות כ-private כדי שגישה ישירה לא תהיה מותרת.
  3. אל תספק שיטות setter למשתנים.
  4. הגדר את כל השדות הניתנים לשינוי כ-final כך שערך של שדה יכול להיות מוקצה רק פעם אחת.
  5. אתחל את כל השדות באמצעות שיטת בנאי שמבצעת העתק עמוק.
  6. בצע שכפול של אובייקטים בשיטות ה-getter כדי להחזיר העתק במקום להחזיר את האובייקט המקורי.

המחלקה הבאה היא דוגמה שמדגימה את היסודות של ללא אפשרות שינוי. המחלקה FinalClassExample מגדירה את השדות ומספקת את שיטת הבנאי שמשתמשת בהעתק עמוק כדי לאתחל את האובייקט. הקוד בשיטת ה-main של הקובץ FinalClassExample.java בודק את הלא-אפשרות לשינוי של האובייקט.

צור קובץ חדש בשם FinalClassExample.java והעתק את הקוד הבא:

FinalClassExample.java
import java.util.HashMap;
import java.util.Iterator;

public final class FinalClassExample {

	// שדות של מחלקת הדוגמה FinalClassExample
	private final int id;
	
	private final String name;
	
	private final HashMap<String,String> testMap;

	
	public int getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	// פונקציית Getter עבור אובייקטים שינויים

	public HashMap<String, String> getTestMap() {
		return (HashMap<String, String>) testMap.clone();
	}

	// שיטת בנאי המבצעת העתק עמוק
	
	public FinalClassExample(int i, String n, HashMap<String,String> hm){
		System.out.println("Performing Deep Copy for Object initialization");

		// המילה המפתח this מפנה לאובייקט הנוכחי
		this.id=i;
		this.name=n;

		HashMap<String,String> tempMap=new HashMap<String,String>();
		String key;
		Iterator<String> it = hm.keySet().iterator();
		while(it.hasNext()){
			key=it.next();
			tempMap.put(key, hm.get(key));
		}
		this.testMap=tempMap;
	}

	// בדיקת המחלקה הלא ניתנת לשינוי

	public static void main(String[] args) {
		HashMap<String, String> h1 = new HashMap<String,String>();
		h1.put("1", "first");
		h1.put("2", "second");
		
		String s = "original";
		
		int i=10;
		
		FinalClassExample ce = new FinalClassExample(i,s,h1);
		
		// הדפסת ערכי ce
		System.out.println("ce id: "+ce.getId());
		System.out.println("ce name: "+ce.getName());
		System.out.println("ce testMap: "+ce.getTestMap());
		// שינוי ערכי המשתנה המקומי
		i=20;
		s="modified";
		h1.put("3", "third");
		// הדפסת הערכים שוב
		System.out.println("ce id after local variable change: "+ce.getId());
		System.out.println("ce name after local variable change: "+ce.getName());
		System.out.println("ce testMap after local variable change: "+ce.getTestMap());
		
		HashMap<String, String> hmTest = ce.getTestMap();
		hmTest.put("4", "new");
		
		System.out.println("ce testMap after changing variable from getter methods: "+ce.getTestMap());

	}

}

קידוד והפעלת התוכנית:

  1. javac FinalClassExample.java
  2. java FinalClassExample

הערה: ייתכן שתקבל את הודעת השגיאה הבאה בעת קידוד הקובץ: Note: FinalClassExample.java משתמש בפעולות שאינן בטוחות או לא מאומתות מכיוון שפונקציית ה-Getter משתמשת בהמרת סוגים לא מאומתת מ-HashMap<String,String> ל-Object. ניתן להתעלם מאזהרת הקומפילציה לצורך דוגמה זו.

תקבל את הפלט הבא:

Output
Performing Deep Copy for Object initialization ce id: 10 ce name: original ce testMap: {1=first, 2=second} ce id after local variable change: 10 ce name after local variable change: original ce testMap after local variable change: {1=first, 2=second} ce testMap after changing variable from getter methods: {1=first, 2=second}

הפלט מציין כי ערכי ה־HashMap לא שונו מכיוון שהבנאי משתמש בהעתק עמוק והפונקציה המקבלת מחזירה עתק של האובייקט המקורי.

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

ניתן לבצע שינויים בקובץ FinalClassExample.java כדי להראות מה קורה כאשר אתה משתמש בהעתקה של צלע במקום העתקה עמוקה ומחזירים את האובייקט במקום העתקה. האובייקט כבר אינו לא משתנה וניתן לשנותו. עשה את השינויים הבאים בקובץ הדוגמה (או העתק והדבק מדוגמת הקוד):

  • מחק את שיטת הבנאי המספקת העתקה עמוקה והוסף את שיטת הבנאי המספקת העתקה של צלע שמודגשת בדוגמה הבאה.
  • בפונקציית הגטר, מחק return (HashMap<String, String>) testMap.clone(); והוסף return testMap;.

הקובץ הדוגמה צריך להיראות כעת כך:

FinalClassExample.java
import java.util.HashMap;
import java.util.Iterator;

public final class FinalClassExample {

	// שדות של מחלקת FinalClassExample
	private final int id;
	
	private final String name;
	
	private final HashMap<String,String> testMap;

	
	public int getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	// פונקציית גטר עבור אובייקטים שיש להם אפשרות לשנות

	public HashMap<String, String> getTestMap() {
		return testMap;
	}

	// שיטת בנאי שמבצעת העתקה של צלע

	public FinalClassExample(int i, String n, HashMap<String,String> hm){
		System.out.println("Performing Shallow Copy for Object initialization");
		this.id=i;
		this.name=n;
		this.testMap=hm;
	}

	// בדוק את המחלקה לא משתנה

	public static void main(String[] args) {
		HashMap<String, String> h1 = new HashMap<String,String>();
		h1.put("1", "first");
		h1.put("2", "second");
		
		String s = "original";
		
		int i=10;
		
		FinalClassExample ce = new FinalClassExample(i,s,h1);
		
		// הדפס את ערכי ה-ce
		System.out.println("ce id: "+ce.getId());
		System.out.println("ce name: "+ce.getName());
		System.out.println("ce testMap: "+ce.getTestMap());
		// שנה את ערכי המשתנה המקומי
		i=20;
		s="modified";
		h1.put("3", "third");
		// הדפס שוב את הערכים
		System.out.println("ce id after local variable change: "+ce.getId());
		System.out.println("ce name after local variable change: "+ce.getName());
		System.out.println("ce testMap after local variable change: "+ce.getTestMap());
		
		HashMap<String, String> hmTest = ce.getTestMap();
		hmTest.put("4", "new");
		
		System.out.println("ce testMap after changing variable from getter methods: "+ce.getTestMap());

	}

}

קומפיל והרץ את התוכנית:

  1. javac FinalClassExample.java
  2. java FinalClassExample

Output
Performing Shallow Copy for Object initialization ce id: 10 ce name: original ce testMap: {1=first, 2=second} ce id after local variable change: 10 ce name after local variable change: original ce testMap after local variable change: {1=first, 2=second, 3=third} ce testMap after changing variable from getter methods: {1=first, 2=second, 3=third, 4=new}

הפלט מראה שערכי ה-HashMap השתנו מכיוון ששיטת הבנאי משתמשת בהעתק רדוד ישנה התייחסות ישירה לאובייקט המקורי בפונקציית הגטר.

מסקנה

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

Source:
https://www.digitalocean.com/community/tutorials/how-to-create-immutable-class-in-java