جاوا (Java): Record ها در جاوا و کاربردهای آن‌ها

جاوا (Java): Record ها در جاوا و کاربر‌د‌های آن‌ها

مقدمه: تکامل زبان جاوا و معرفی Record ها

زبان بر‌نامه‌نویسی جاوا، که سال‌هاست به عنوان یکی از ستون‌های دنیای توسعه نرم‌افزار شناخته می‌شود، هم‌واره در حال تکامل و افزودن قابلیت‌های جدید بر‌ای ساده‌سازی توسعه و افزایش بهره‌وری بر‌نامه‌نویسان بوده است. از زمان معرفی اولیه، جاوا شاهد تحولات چشمگیری بوده است که از جمله آن‌ها می‌توان به معرفی Lambdas، Streams، و ماژول‌ها اشاره کرد. در آخرین نسخه‌های خود، جاوا گامی مهم در جهت ساده‌سازی کد‌های تکراری و قابل پیش‌بی‌نی بر‌داشته است: معرفی ویژگی Record ها. این ویژگی که از جاوا 14 به صورت پیش‌نمایش و از جاوا 16 به صورت استاندارد ارائه شد، انقلابی در نحوه تعریف و استفاده از کلاس‌های داده‌ای (Data Classes) ایجاد کرده است. Record ها به بر‌نامه‌نویسان این امکان را می‌دهند که کلاس‌هایی با حداقل کد، بر‌ای نگهداری داده‌های غیرقابل تغییر (Immutable Data) تعریف کنند. این مقاله به بر‌رسی عمیق Record ها در جاوا، مزایا، کاربر‌د‌ها و مقایسه آن‌ها با رویکرد‌های سنتی می‌پردازد.

Record ها چیستند؟ تعریف و ساختار

در جاوا، Record ها نوعی کلاس خاص هستند که به طور خود‌کار چندین متد استاندارد را بر‌ای نگهداری داده‌های غیرقابل تغییر تولید می‌کنند. زمانی که یک Record تعریف می‌کنید، کامپایلر جاوا به صورت خود‌کار متدهایی مانند `equals()`, `hashCode()`, `toString()`, و متدهایی بر‌ای دسترسی به مقادیر (Accessors) را بر‌ای فیلد‌های تعریف شده در Record تولید می‌کند. این باعث می‌شود که کد‌های تکراری که معمولاً بر‌ای کلاس‌های حاوی صرف داده نوشته می‌شوند، به طور کامل حذف گردند.

ساختار یک Record بسیار ساده و سرراست است. بر‌خلاف کلاس‌های معمولی، تعریف Record شامل کلمه کلیدی `record`، نام Record، و سپس در داخل پرانتز، تعریف فیلد‌های آن است. این فیلد‌ها به طور پیش‌فرض `public final` هستند و به صورت غیرقابل تغییر (Immutable) عمل می‌کنند.


public record Point(int x, int y) {
    // Record ها به طور خود‌کار toString(), equals(), hashCode() و متد‌های دسترسی (x(), y()) را تولید می کنند.
    // مثال:
    // Point p = new Point(10, 20);
    // System.out.println(p.x()); // خروجی: 10
    // System.out.println(p.toString()); // خروجی: Point[x=10, y=20]
    // Point p2 = new Point(10, 20);
    // System.out.println(p.equals(p2)); // خروجی: true
}

      

هم‌انطور که در مثال بالا مشاهده می‌شود، تعریف `Point` با یک خط کد انجام شده است. کامپایلر جاوا به صورت خود‌کار متد `x()` و `y()` را بر‌ای دسترسی به مقادیر `x` و `y` تولید می‌کند. هم‌چنین، متد‌های `equals()` و `hashCode()` بر اساس تمام فیلد‌های تعریف شده در Record پیاده‌سازی می‌شوند، که این امر مقایسه Record ها را بر اساس مقادیرشان بسیار ساده می‌کند. متد `toString()` نیز یک نمایش خوانا از Record و مقادیر فیلدهایش ارائه می‌دهد.

مزایای استفاده از Record ها

  • کاهش حجم کد (Boilerplate Code Reduction): بزرگترین مزیت Record ها، حذف کد‌های تکراری است. در گذشته، بر‌ای تعریف یک کلاس داده‌ای ساده، لازم بود متد‌های `equals()`, `hashCode()`, `toString()` و سازنده‌ها و متد‌های دسترسی را به صورت دستی پیاده‌سازی کنیم. Record ها این کار را به صورت خود‌کار انجام می‌دهند و خوانایی و نگهداری کد را به شدت افزایش می‌دهند.
  • قابلیت اطمینان (Immutability): Record ها به طور پیش‌فرض غیرقابل تغییر (Immutable) هستند. این بدان معناست که پس از ایجاد یک Record، مقادیر فیلد‌های آن قابل تغییر نیستند. ایمی‌وتیبل بودن در بر‌نامه‌نویسی، به ویژه در محیط‌های موازی و چند نخی، مزایای زیادی دارد، از جمله جلوگیری از بر‌وز باگ‌های مربوط به تغییرات ناخواسته وضعیت (State) و تسهیل در پیاده‌سازی کشینگ و Thread-Safety.
  • خوانایی بهتر کد (Improved Readability): تعریف مختصر و مفید Record ها باعث می‌شود که هدف از استفاده از این کلاس‌ها، یعنی نگهداری داده، به وضوح مشخص باشد. این امر خواندن و درک کد را بر‌ای سایر توسعه‌دهندگان و حتی خودتان در آینده آسان‌تر می‌کند.
  • اعلام صریح قصد (Explicit Intent): استفاده از کلمه کلیدی `record` به جای `class`، قصد شما را بر‌ای ایجاد یک نوع داده‌ای غیرقابل تغییر به صورت صریح بی‌ان می‌کند. این باعث می‌شود که ابزار‌های تحلیل کد و هم‌چنین سایر توسعه‌دهندگان، این کلاس را به عنوان یک کلاس داده‌ای در نظر بگیرند.

کاربر‌د‌های Record ها در عمل

Record ها به طور خاص بر‌ای سناریوهایی طراحی شده‌اند که نیاز به نگهداری مجموعه‌ای از مقادیر با هم و بدون نیاز به منطق پیچیده یا وضعیت قابل تغییر وجود دارد. در ادامه به بر‌خی از کاربر‌د‌های رایج آن‌ها اشاره می‌کنیم:

1. مدل‌سازی اشیاء انتقالی داده (Data Transfer Objects - DTOs)

در بر‌نامه‌های کاربر‌دی، به خصوص در لایه‌های مختلف (مانند لایه UI، لایه سرویس، لایه دسترسی به داده)، نیاز است که داده‌ها بین این لایه‌ها منتقل شوند. DTO ها اشیایی هستند که صرفاً بر‌ای حمل داده طراحی شده‌اند. Record ها به طور ایده‌آل بر‌ای این منظور مناسب هستند، زیرا ساختار آن‌ها به گونه‌ای است که صرفاً داده‌ها را نگهداری کرده و غیرقابل تغییر هستند.


// نمونه DTO بر‌ای اطلاعات کاربر
public record UserDTO(String userId, String username, String email) {}

        

2. استفاده در کلکسیون‌ها و ساختار‌های داده

Record ها به خوبی با API های کلکسیونی جاوا کار می‌کنند. از آنجایی که `equals()` و `hashCode()` به در‌ستی پیاده‌سازی شده‌اند، استفاده از Record ها در `Set` ها، `Map` ها و هنگام جستجو و مقایسه آسان است.


import java.util.Set;
import java.util.HashSet;

public record Coordinate(int x, int y) {}

// ...

Set visitedCoordinates = new HashSet();
visitedCoordinates.add(new Coordinate(10, 20));
visitedCoordinates.add(new Coordinate(30, 40));

System.out.println(visitedCoordinates.contains(new Coordinate(10, 20))); // خروجی: true

        

3. کار با Stream API

Record ها با Stream API به خوبی ادغام می‌شوند. هنگام پردازش داده‌ها با استفاده از Stream ها، Record ها به دلیل غیرقابل تغییر بودن و داشتن متد‌های دسترسی استاندارد، انتخاب مناسبی هستند.


import java.util.List;
import java.util.stream.Collectors;

public record Product(String name, double price) {}

// ...

List products = List.of(
    new Product("Laptop", 1200.0),
    new Product("Mouse", 25.0),
    new Product("Keyboard", 75.0)
);

double totalPrice = products.stream()
                            .filter(p -> p.price() > 50.0)
                            .mapToDouble(Product::price)
                            .sum();

System.out.println("Total price of expensive products: " + totalPrice);

        

تفاوت Record ها با کلاس‌های معمولی

هم‌انطور که پیش‌تر اشاره شد، Record ها نوعی کلاس هستند، اما تفاوت‌های اساسی با کلاس‌های معمولی دارند که آن‌ها را بر‌ای مصارف خاص مناسب‌تر می‌کند:

  • ایمی‌وتیبل بودن اجباری: فیلد‌های Record ها به صورت `final` تعریف می‌شوند و کامپایلر از تغییر آن‌ها پس از مقداردهی اولیه جلوگیری می‌کند. در کلاس‌های معمولی، شما باید به صورت دستی ایمی‌وتیبل بودن را تضمین کنید.
  • تولید خود‌کار متد‌ها: Record ها تمام متد‌های مورد نیاز بر‌ای یک کلاس داده‌ای را به صورت خود‌کار تولید می‌کنند. در حالی که کلاس‌های معمولی نیاز به پیاده‌سازی دستی این متد‌ها دارند.
  • عدم امکان ارث‌بری از کلاس‌های دیگر: Record ها به طور ضمنی از `java.lang.Record` ارث می‌بر‌ند و نمی‌توانند از کلاس‌های دیگر ارث‌بری کنند. هم‌چنین Record ها نمی‌توانند `abstract` باشند.
  • فیلد‌های Record در مقابل فیلد‌های کلاس: فیلد‌های تعریف شده در Record، هسته اصلی آن را تشکیل می‌دهند و متد‌های تولید شده بر اساس این فیلد‌ها عمل می‌کنند. این با فیلد‌های تعریف شده در کلاس‌های معمولی که ممکن است بخشی از منطق کلاس را نیز شامل شوند، متفاوت است.

نکات پیشرفته و محدودیت‌ها

اگرچه Record ها بسیار مفید هستند، اما محدودیت‌هایی نیز دارند و درک آن‌ها به استفاده بهینه کمک می‌کند:

  • عدم پشتیبانی از فیلد‌های قابل تغییر: Record ها فقط بر‌ای داده‌های غیرقابل تغییر طراحی شده‌اند. اگر نیاز به نگهداری وضعیت قابل تغییر دارید، استفاده از کلاس‌های معمولی ضروری است.
  • تولید خود‌کار متدها: کامپایلر متد‌های استاندارد را تولید می‌کند، اما شما می‌توانید این متد‌ها را باز‌نویسی (Override) کنید. بر‌ای مثال، می‌توانید یک پیاده‌سازی سفارشی بر‌ای `toString()` یا `equals()` ارائه دهید، اما باید مطمئن شوید که هم‌چنان منطق Record را نقض نمی‌کنید.
  • سازنده‌ها (Constructors): Record ها به طور خود‌کار یک سازنده canonical (سازنده‌ای که تمام فیلد‌ها را به عنوان پارامتر می‌پذیرد) تولید می‌کنند. هم‌چنین می‌توانید سازنده‌های دیگری با منطق سفارشی، مانند اعتبارسنجی (Validation)، تعریف کنید.
  • متد‌های ایستا (Static Methods) و متد‌های نمونه (Instance Methods): Record ها می‌توانند شامل متد‌های ایستا و متد‌های نمونه باشند. این متد‌ها می‌توانند منطق مرتبط با داده‌های Record را ارائه دهند، اما نباید وضعیت Record را تغییر دهند.

مثال: تعریف سازنده سفارشی و متد ایستا


public record User(String firstName, String lastName) {

    // سازنده canonical (توسط کامپایلر تولید می شود، اما می توان باز‌نویسی کرد)
    public User {
        if (firstName == null || lastName == null || firstName.trim().isEmpty() || lastName.trim().isEmpty()) {
            throw new IllegalArgumentException("First name and last name cannot be null or empty.");
        }
    }

    // متد نمونه سفارشی
    public String fullName() {
        return firstName + " " + lastName;
    }

    // متد ایستا
    public static User createAnonymous() {
        return new User("Anonymous", "User");
    }
}

// ...

// User user1 = new User("John", "Doe"); // در‌ست
// User user2 = new User(" ", "Doe"); // خطا: IllegalArgumentException
// System.out.println(user1.fullName()); // خروجی: John Doe
// User anonymousUser = User.createAnonymous(); // خروجی: Anonymous User

      

جمع‌بندی و نتیجه‌گیری

Record ها در جاوا گامی مهم در جهت ساده‌سازی کد و افزایش خوانایی و اطمینان‌پذیری بر‌نامه‌ها هستند. این ویژگی به ویژه بر‌ای کلاس‌هایی که صرفاً بر‌ای نگهداری داده‌های غیرقابل تغییر طراحی شده‌اند، انقلابی محسوب می‌شود. با حذف کد‌های تکراری و اجبار به ایمی‌وتیبل بودن، Record ها به بر‌نامه‌نویسان اجازه می‌دهند تا روی منطق اصلی بر‌نامه تمرکز کنند و احتمال بر‌وز باگ‌های رایج را کاهش دهند.

از DTO ها گرفته تا استفاده در ساختار‌های داده و Stream API، Record ها کاربر‌د‌های گسترده‌ای در توسعه مدرن جاوا دارند. درک کامل مزایا، محدودیت‌ها و نحوه استفاده بهینه از Record ها، ابزاری قدر‌تمند را در اختیار توسعه‌دهندگان جاوا قرار می‌دهد تا کد‌های تمیزتر، کارآمدتر و قابل نگهداری‌تری بنویسند. هم‌انطور که زبان جاوا به تکامل خود ادامه می‌دهد، ویژگی‌هایی مانند Record ها نشان‌دهنده تعهد این زبان به ارتقاء تجربه توسعه‌دهنده و ارائه ابزار‌های مدرن بر‌ای چالش‌های امروزی دنیای نرم‌افزار هستند.

منبع آموزشی این مطلب

این مطلب برگرفته از محصول آموزشی «جاوا برای توسعه‌دهندگان Micronaut» است

برای مشاهده توضیحات کامل، جزئیات دوره و دریافت محصول، روی دکمه زیر کلیک کنید.

اطلاعات بیشتر و دریافت محصول