Java - Record
החל מ JAVA 16 ומעלה, האופציה של שימוש ב record פתוחה לנו כמפתחים.
למה אנחנו צריכים Record?
אם היינו רוצים מחלקה שתחזיק לנו נתונים "יבשים" כמו גיל, שם, טלפון וכו' היינו צריכים לכתוב מחלקה עם מלא קוד: קונסטרקטור, Getter ים, ()equals(), hashCode, ו() toString.
Record בא לייתר לנו את כתיבת הקוד המלא, הוא מחלקה מיוחדת שמוגדרת כנתונים טהורים (Data Carrier). ברגע שנגדיר Record, הקומפיילר של Java מייצר עבורנו אוטומטית את כל הקונסטרקטורים, המתודות וההשוואות מאחורי הקלעים.
איך מייצרים record?
שורה אחת ויש לנו record!
public record User(String name, int age, String phoneNumber) { }
איך משתמשים ב record?
ניצור רשומה עם כל השדות שהגדרנו, ואז פשוט אפשר לקרוא לכל פיסת מידע שהוספנו בקוד.
כפי שניתן לראות בקוד ה toString כבר נוצר לנו אוטומטית, ללא כל מעורבות שלנו.
User user = new User("Alice", 30, "050 12345678");
System.out.println(user.name()); // "Alice"
System.out.println(user); // User[name=Alice, age=30, phoneNumber=050 12345678]
מה השימושים העיקריים ב record?
- העברת נתונים DTOs / Data Transfer Objects: מעבר נתונים בין שכבות במערכת או קבלת מידע מ API חיצוני.
- תתי מחלקות של Sealed Classes: . אם אתם לא מכירים את sealed class ניתן לקרוא עוד על הנושא כאן.
- כאשר הנתונים קבועים מראש: כשאנחנו יודעים שברגע שהאובייקט נוצר, הערכים שלו לא אמורים להשתנות לעולם.
ירושה של Record
- הם final: אי אפשר לרשת מחלקת Record אחרת, ואי אפשר ליצור תת מחלקה מתוך Record קיים.
- הם יורשים אוטומטית מ java.lang.Record: בדיוק כמו שכל מחלקה רגילה יורשת מ Object, כל Record יורש מאב קצה בשם java.lang.Record. מכיוון ש Java אינה תומכת בירושה ריבוי מחלקות, Record אינו יכול לרשת מחלקה אחרת.
- מימוש implements: למרות שהם לא יכולים לרשת מחלקות, הם לגמרי יכולים לממש ממשקים (כפי שראינו ב sealed class).
Record הם אימוטביליים (Immutable) לחלוטין: כל השדות ב Record מוגדרים אוטומטית כ private final. אי אפשר לשנות את הערך של שדה אחרי שה Record נוצר אין Setter. אם צריך ערך שונה, יוצרים אובייקט חדש. רוצים להרחיב על נושא ה Immutable? כאן תמצאו עוד מידע עליו.
יתכנו בעיות עם פריימוורקים ישנים (כמו JPA/Hibernate): פריימוורקים מסוימים לניהול בסיסי נתונים דורשים קונסטרקטור ריק (No args constructor) ואפשרות לשנות שדות (Setters). מכיוון של Records אין את אלה, עבודה איתם בתוך ישויות DB דורשת לפעמים התאמות או ספריות צד שלישי כמו Jackson ב JSON שדווקא תומך בהם מעולה.
אי אפשר להוסיף שדות מנוהלים נוספים: כל השדות חייבים להיות מוגדרים ישירות בכותרת של ה Record, השדות שהוגדרו במלואם וללא חסרים.
שדות ומתודות סטטיים
למרות שאסור להוסיף שדות רגילים, כלומר שדות שניתנים לשינוי עם set, מותר לגמרי להוסיף שדות סטטיים: כמו קבועים (public static final String DEFAULT_NAME = "Guest") ומתודות סטטיות.
זה נפוץ מאוד למשל כשנרצה ליצור מתודת Factory (כמו of(...)) שמייצרת את ה Record בצורה חכמה. במקום שקוד חיצוני יצטרך לקרוא ישירות ל new User(...) ולהסתבך עם העברת פרמטרים או ברירת מחדל, אנחנו קוראים למתודה בעלת משמעות (כמו User.of או User.createGuest)) שתעשה את העבודה מאחורי הקלעים: היא יכולה להוסיף ערכי ברירת מחדל, לנקות נתונים, או אפילו להחליט איזו גרסה של אובייקט ליצור בהתאם למה שהועבר.
public record User(String name, int age, String phoneNumber) {
// 1. Static constant fields shared across all record instances
public static final int DEFAULT_AGE = 18;
public static final String DEFAULT_PHONE = "050 00000000";
// Compact Constructor for standard validation
public User {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be null or blank!");
}
}
// 2. Static Factory Method: Creates a User with a default phone number
public static User of(String name, int age) {
// Calls the standard constructor with a fallback phone number
return new User(name, age, DEFAULT_PHONE);
}
// 3. Static Factory Method: Creates an adult user with a default age
public static User createAdult(String name, String phoneNumber) {
// Uses the static constant DEFAULT_AGE
return new User(name, DEFAULT_AGE, phoneNumber);
}
}
איך נשתמש במתודות שיצרנו? כמו כל מתודה אחרת בקוד (דוגמת רצה ב MAIN):
public class Main {
public static void main(String[] args) {
// Using static factory method 'of' (provides default phone automatically)
User user1 = User.of("Alice", 25);
System.out.println(user1);
// Output: User[name=Alice, age=25, phoneNumber=050 00000000]
// Using static factory method 'createAdult' (provides default age automatically)
User user2 = User.createAdult("Bob", "054 12345678");
System.out.println(user2);
// Output: User[name=Bob, age=18, phoneNumber=054 12345678]
// Accessing the static constant fields directly from the Record name
System.out.println("Default user age is: " + User.DEFAULT_AGE);
}
}
Compact Constructor
למרות שלא נדרש Constructor ב record, ניתן להוסיף Constructor על מנת ליצור ולדיציה לנתונים שלנו.
איך זה עובד?
ב Compact Constructor, אנחנו לא כותבים את הפרמטרים בסוגריים ולא מעבירים מחדש את השדות ל this. Java כבר יודעת לבד מה הפרמטרים כי הגדרנו אותם בשורת ה Record הראשית. אנחנו פשוט ניגשים לשדות ישירות ובודקים אותם לפני שהם נשמרים.
דוגמת קוד:
נניח שאנחנו רוצים לוודא שגיל של משתמש לא יכול להיות שלילי, וששם המשתמש לעולם אינו ריק או null:
public record User(String name, int age, String phoneNumber) {
// Compact Constructor - no parameter parentheses required!
public User {
// 1. Check if the name is null or blank
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be null or blank!");
}
// 2. Check if the age is valid
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative: " + age);
}
// 3. Validate the phone number:
// Must not be null
// Must be strictly greater than 12 characters long
// Must have a hyphen (' ') exactly after the first 3 characters (index 3)
// All other characters (before and after the hyphen) must be digits only
if (phoneNumber == null || phoneNumber.length() != 12) {
throw new IllegalArgumentException("Phone number must be exactly 12 characters long!");
}
if (phoneNumber.charAt(3) != ' ') {
throw new IllegalArgumentException("Phone number must contain a hyphen (' ') after the first 3 characters!");
}
// Validate that everything else consists strictly of digits
for (int i = 0; i < phoneNumber.length(); i++) {
if (i == 3) continue; // Skip the hyphen at index 3
if (!Character.isDigit(phoneNumber.charAt(i))) {
throw new IllegalArgumentException("Phone number can only contain digits (except for the hyphen at index 3)!");
}
}
// No need to write this.name = name; > Java assigns the fields automatically at the end of the block!
}
}
מה מיוחד בזה?
- קוד מובן:this.name = name; וכו'. כאן, הקומפיילר מטפל בהשמת הערכים באופן אוטומטי, מיד אחרי Compact Constructor שלנו מסיים את ריצתו בהצלחה.
- חסינות מוחלטת:new User("", 5, "050 12345678"), Compact Constructor יעצור את הכל, יזרוק את השגיאה, ואי אפשר יהיה לייצר אובייקט לא תקין במערכת.
זה הופך את ה Records לא רק למחלקות נתונים נוחות, אלא גם ליותר בטוחות באמצעות ולידציות.