אובייקטים בלתי משתנים (Immutable Objects)

אובייקטים בלתי משתנים (Immutable Objects) הם אחת מהתפיסות החשובות והחזקות ביותר בתכנות מונחה עצמים (OOP) ובתכנות מקבילי (Concurrency).
הכלל הבסיסי פשוט: מרגע שנוצר אובייקט Immutable, אי אפשר לשנות את הערכים (השדות) שלו לאורך כל מחזור החיים שלו. אם נרצה לשנות משהו, נצטרך ליצור אובייקט חדש לגמרי.

איך מייצרים Immutable Object ב Java?

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

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

public final class ImmutablePerson {
 private final String name;
private final int age;
private final List<String> hobbies;
public ImmutablePerson(String name, int age, List<String> hobbies) { this.name = name; this.age = age;
//deep copy rule
 this.hobbies = new ArrayList<>(hobbies);
}
 public String getName()
 { return name; }
 public int getAge()
 { return age; }
 public List<String> getHobbies()
//new copy!
{ return new ArrayList<>(hobbies); }
 }

יתרונות בשימוש Immutable Object:

הקשר ל-String Pooling (אופטימיזציית זיכרון)

הסיבה המרכזית לכך שמחלקת String ב Java היא Immutable היא כדי לאפשר String Pooling. מכיוון שהמחרוזת בלתי משתנה, Java יכולה לשמור מופע (Instance) אחד של המחרוזת בזיכרון (בתוך ה String Pool) ולתת למספר משתנים שונים להצביע אליו בבטחה מלאה. אם String היה משתנה, שינוי אצל משתמש אחד היה הורס את הערך לכל שאר המשתנים במערכת. ניצן לקרוא עוד על String Pooling כאן

שיפורים החל מ JAVA 14

Java Records (החל מ Java 14): אם אנחנו צריכים אובייקט שהוא נטו "מיכל נתונים" (Data Carrier), Java מציעה את ה record. רשומות הן Immutable בצורה מובנית ומוצהרת, וחוסכות לנו לכתוב את רוב הקוד (Constructors, Getters, equals, hashCode, toString). מחלקה מסוג record עושה את כל זה אוטומטית בשורה אחת, והיא Immutable (בלתי משתנה) מטבעה. יצירת הקונסטרוקטור מתבצעת אוטומטית, ה get מתקבלים אוטומטית גם, equals and hashCode גם מגיעים בצורה אוטומטית.

לדוגמה ניתן לכתוב את המחלקה הבאה:

public record UserRecord(int id, String username, String email) {}

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

// 1. Define a Record in a single concise line!
public record UserRecord(int id, String username, String email) {

 // Compact Constructor: Perform validations without boilerplate code
public UserRecord {
 if (id <= 0) {
 throw new IllegalArgumentException("ID must be positive"); }
 if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email address"); }
}
// Add a custom helper method public void printSummary() { System.out.println("User: " + username + " (" + email + ")");
}
 }

שימוש במחלקה:

public class Main {
    public static void main(String[] args) {
        // Create an object (no complex new logic needed, compiler generates the constructor automatically)
        UserRecord user1 = new UserRecord(1, "john_doe", "john@example.com");

        // Use the automatic getters (method name is directly the field name, without the 'get' prefix!)
        System.out.println(user1.username()); // Output: john_doe
        System.out.println(user1.email());    // Output: john@example.com

        // Use the custom method we added
        user1.printSummary();

        // toString() is built-in and provides a nice automatic format:
        System.out.println(user1);
        // Output: UserRecord[id=1, username=john_doe, email=john@example.com]

        // Equality comparison (equals and hashCode are automatically implemented based on values!)
        UserRecord user2 = new UserRecord(1, "john_doe", "john@example.com");
        System.out.println(user1.equals(user2)); // true (because the values are identical)

    }
}
🏠 Back to Orly's Code Corner