۲۸ مهر ۱۴۰۴

Techboy

اخبار و اطلاعات روز تکنولوژی

مقدمه‌ای بر رکوردهای جاوا: برنامه‌نویسی داده‑محور ساده‌شده در جاوا

اعلان‌های رکورد روشی کارآمد برای بسته‌بندی داده‌ها در کلاس‌های جاوای شما هستند که در عین حال کدهای تکراری را کاهش می‌دهند. بیاموزید که چگونه در هر دو سناریوی برنامه‌نویسی پایه و پیشرفته عمل می‌کنند.

اعلان‌های رکورد روشی کارآمد برای بسته‌بندی داده‌ها در کلاس‌های جاوای شما هستند که در عین حال کدهای تکراری را کاهش می‌دهند. بیاموزید که چگونه در هر دو سناریوی برنامه‌نویسی پایه و پیشرفته عمل می‌کنند.

رکوردها در جاوا نوع جدیدی از کلاس برای نگهداری داده هستند. به‌جای نوشتن کدهای تکراری برای سازنده‌ها، دسترسی‌گرها، equals()، hashCode() و toString()، فقط فیلدها را اعلام می‌کنید و اجازه می‌دهید کامپایلر جاوا بقیه را مدیریت کند. این مقاله شما را با رکوردهای جاوا آشنا می‌کند، شامل مثال‌های استفاده پایه و پیشرفته و چندین سناریوی برنامه‌نویسی که نباید از آن‌ها استفاده کنید.

توجه: رکوردهای جاوا در JDK 16 نهایی شدند.

چگونه کامپایلر جاوا کلاس‌های رکورد را مدیریت می‌کند

ساخت کلاس‌های داده ساده در جاوا به‌صورت سنتی نیاز به کدهای تکراری قابل توجهی داشت. در نظر بگیرید چگونه ماسکوت‌های جاوا، Duke و Juggy را نمایش می‌دهیم:

public class JavaMascot { private final String name; private final int yearCreated; public JavaMascot(String name, int yearCreated) { this.name = name; this.yearCreated = yearCreated; } public String getName() { return name; } public int getYearCreated() { return yearCreated; } // equals, hashCode and toString methods omitted for brevity }

با رکوردها می‌توانیم کد بالا را به یک خط کاهش دهیم:

public record JavaMascot(String name, int yearCreated) {}

این اعلام مختصر به‌طور خودکار فیلدهای خصوصی نهایی، سازنده، متدهای دسترسی، و متدهای equals()، hashCode() و toString() به‌درستی پیاده‌سازی شده را فراهم می‌کند.

حالا که رکورد JavaMascot را تعریف کردیم، می‌توانیم آن را به کار بگیریم:

public class RecordExample { public static void main(String[] args) { JavaMascot duke = new JavaMascot("Duke", 1996); JavaMascot juggy1 = new JavaMascot("Juggy", 2005); JavaMascot juggy2 = new JavaMascot("Juggy", 2005); System.out.println(duke); // JavaMascot[name=Duke, yearCreated=1996] System.out.println(juggy1.equals(juggy2)); // true System.out.println(duke.equals(juggy1)); // false System.out.println("Mascot name: " + duke.name()); System.out.println("Created in: " + duke.yearCreated()); } }

رکوردها به‌طور خودکار نمایش رشته‌ای معنادار، مقایسه مساوی‌سازی بر پایه مقدار، و متدهای دسترسی ساده‌ای که با نام‌های اجزاء مطابقت دارند را فراهم می‌کنند.

سفارشی‌سازی رکوردها

اگرچه رکوردها به‌طور ذاتی مختصر هستند، هنوز می‌توانید آن‌ها را با رفتارهای سفارشی ارتقاء دهید. به مثال‌های زیر نگاهی بیندازید.

سازنده‌های فشرده

رکوردها سینتکس ویژه «سازنده فشرده» را فراهم می‌کنند که به شما اجازه می‌دهد پارامترهای ورودی را بدون تکرار لیست پارامترها اعتبارسنجی یا تبدیل کنید:

record JavaMascot(String name, int yearCreated) { // Compact constructor with validation public JavaMascot { if (name == null || name.isBlank()) { throw new IllegalArgumentException("Name cannot be empty"); } if (yearCreated < 1995) { throw new IllegalArgumentException("Java mascots didn't exist before 1995"); } } }

سازنده فشرده پس از مقداردهی فیلدها اما پیش از ساخت کامل شیء اجرا می‌شود، که برای اعتبارسنجی ایده‌آل است. در این مثال، اعلان پارامترها حذف شده‌اند، اما به‌صورت ضمنی در سازنده در دسترس هستند.

اضافه کردن متدها

ما همچنین می‌توانیم متدهایی به رکوردها اضافه کنیم:

record JavaMascot(String name, int yearCreated) { public boolean isOriginalMascot() { return name.equals("Duke"); } public int yearsActive() { return java.time.Year.now().getValue() - yearCreated; } }

متدها به رکوردها امکان می‌دهند رفتار مرتبط با داده‌های خود را محصور کنند در حالی که سینتکس مختصر و غیرقابل تغییر را حفظ می‌کند.

حالا نگاهی به برخی روش‌های پیشرفته‌تر استفاده از رکوردهای جاوا می‌اندازیم.

تطبیق الگو با instanceof و switch

رکوردها به‌عنوان بخشی حیاتی از مطابقت الگو در جاوا ۲۱، با پشتیبانی در عبارات switch، تجزیه اجزاء، الگوهای تو در تو و شرایط محافظت‌کننده، ظاهر شدند.

زمانی که با عملگر پیشرفته instanceof ترکیب می‌شوند، رکوردها به‌صورت مختصر امکان استخراج اجزاء در طول اعتبارسنجی نوع را می‌دهند:

record Person(String name, int age) {} if (obj instanceof Person person) { System.out.println("Name: " + person.name()); }

حالا به یک مثال سنتی‌تر می‌پردازیم. اشکال هندسی راهی کلاسیک برای نشان دادن نحوه کار رابط‌های sealed با رکوردها هستند و تطبیق الگو را به‌ویژه واضح می‌کنند. زیبایی این ترکیب در عبارات switch (معرفی شده در جاوا ۱۷) مشهود است که به شما امکان نوشتن کد مختصر و ایمن از نظر نوع را می‌دهد که شبیه به انواع داده جبری در زبان‌های تابعی است.

sealed interface Shape permits Rectangle, Circle, Triangle {} record Rectangle(double width, double height) implements Shape {} record Circle(double radius) implements Shape {} record Triangle(double base, double height) implements Shape {} public class RecordPatternMatchingExample { public static void main(String[] args) { Shape shape = new Circle(5); // Expressive, type-safe pattern matching double area = switch (shape) { case Rectangle r -> r.width() * r.height(); case Circle c -> Math.PI * c.radius() * c.radius(); case Triangle t -> t.base() * t.height() / 2; }; System.out.println("Area = " + area); } }

در اینجا، نوع Shape یک رابط sealed است که فقط Rectangle، Circle و Triangle را مجاز می‌کند. از آنجا که این مجموعه بسته است، سوئیچ به‌طور کامل پوشش داده می‌شود و نیازی به شاخه پیش‌فرض ندارد.

تطبیق الگو در جاوا

برای کاوش بیشتر در رکوردهای جاوا با تطبیق الگو، آموزش اخیر من، Basic and advanced pattern matching in Java، را ببینید.

استفاده از رکوردها به‌عنوان اشیای انتقال داده

رکوردها به‌عنوان اشیای انتقال داده (DTO) در طراحی‌های مدرن API مانند REST، GraphQL، gRPC یا ارتباط بین سرویس‌ها درخشان هستند. سینتکس مختصر و برابری داخلی آن‌ها، رکوردها را برای نگاشت بین لایه‌های سرویس ایده‌آل می‌سازد. در اینجا یک مثال آمده است:

record UserDTO(String username, String email, Set<String> roles) {} record OrderDTO(UUID id, UserDTO user, List<ProductDTO> items, BigDecimal total) {}

DTOها در برنامه‌های میکروسرویس‌ها فراوان هستند. استفاده از یک رکورد آن‌ها را به‌ دلیل عدم تغییرپذیری مستحکم‌تر می‌کند و تمیزتر، چون نیازی به نوشتن سازنده‌ها، گترها یا متدهایی مانند equals() و hashCode() ندارید.

رکوردها در برنامه‌نویسی تابعی و همزمان

رکوردها به‌عنوان مخازن داده‌ی غیرقابل تغییر، برنامه‌نویسی تابعی و همزمانی را تکمیل می‌کنند. آن‌ها به‌عنوان نوع بازگشتی توابع خالص، در پایپلاین‌های جریان و برای به‌اشتراک‌گذاری ایمن داده‌ها بین نخ‌ها عمل می‌کنند.

از آنجا که فیلدها نهایی و غیرقابل تغییر هستند، رکوردها از یک دسته کامل مشکلات Threading جلوگیری می‌کنند. پس از ساخت، وضعیت آن‌ها نمی‌تواند تغییر کند، بنابراین بدون کپی‌گیری محافظتی یا همگام‌سازی، ایمن برای نخ‌ها هستند. این مثال را در نظر بگیرید:

transactions.parallelStream().mapToDouble(Transaction::amount).sum();

از آنجا که رکوردها غیرقابل تغییر هستند، این محاسبه‌ی موازی به‌ طور ذاتی ایمن برای نخ‌هاست.

زمانی که نباید از رکوردهای جاوا استفاده کنید

تا به‌حال دیده‌ایم که رکوردها در کجا می‌درخشند، اما آن‌ها جایگزین کلی نیستند. برای مثال، هر رکورد به‌طور ضمنی از java.lang.Record ارث می‌برد، بنابراین رکوردها نمی‌توانند از کلاس دیگری ارث ببرند (اگرچه می‌توانند اینترفیس‌ها را پیاده کنند). رکوردها در سناریوهایی که وراثت کلاس لازم است مناسب نیستند.

بیایید برخی دیگر از موقعیت‌هایی که در آن رکوردهای جاوا کمبود دارند را بررسی کنیم.

رکوردها به‌طور ذاتی غیرقابل تغییر هستند

اجزاء رکورد همیشه نهایی هستند، بنابراین در برنامه‌هایی که به اشیای قابل تغییر/ حالت‌دار نیاز دارند مناسب نیستند. مثال زیر یک کلاس قابل تغییر را نشان می‌دهد که به تغییر وضعیت وابسته است، که رکوردها اجازه آن را نمی‌دهند:

public class GameCharacter { private int health; private Position position; public void takeDamage(int amount) { this.health = Math.max(0, this.health - amount); } public void move(int x, int y) { this.position = new Position(this.position.x() + x, this.position.y() + y); } }

آن‌ها رفتار پیچیده را مدل نمی‌کنند

طرح‌های متمرکز بر وضعیت تغییرپذیر، منطق تجاری سنگین یا الگوهایی مانند استراتژی، بازدیدکننده یا نظاره‌گر بهتر با استفاده از کلاس‌های سنتی پیاده می‌شوند. در اینجا یک مثال از منطق پیچیده‌ای که با رکورد سازگار نیست آورده شده است:

public class TaxCalculator { private final TaxRateProvider rateProvider; private final DeductionRegistry deductions; public TaxAssessment calculateTax(Income income, Residence residence) { // Complex logic that doesn’t suit a record } }

آن‌ها با برخی چارچوب‌ها ناسازگار هستند

برخی چارچوب‌ها، به‌ویژه ORMها، ممکن است با رکوردها به‌خوبی کار نکنند. ابزارهای سریال‌سازی یا بازتاب‌گر سنگین نیز ممکن است مشکل داشته باشند. همیشه سازگاری ویژگی‌های جاوا با پشته فناوری خود را بررسی کنید:

// May not work well with some ORM frameworks record Employee(Long id, String name, Department department) {} // Instead, you might need a traditional entity class @Entity public class Employee { @Id @GeneratedValue private Long id; private String name; @ManyToOne private Department department; // Getters, setters, equals, hashCode, etc. }

این نکات به این معنی نیست که رکوردها ناکامل هستند؛ آن‌ها فقط نشان می‌دهند که رکوردها برای نقش‌های خاصی طراحی شده‌اند. در برخی موارد، کلاس‌های سنتی عملی‌تر هستند.

رکوردها و سریال‌سازی در جاوا

رکوردها به‌طور گسترده‌ای در سراسر اکوسیستم جاوا اتخاذ شده‌اند و عدم تغییرپذیری آن‌ها آن‌ها را برای نگهداری، پیکربندی و انتقال داده‌ها جذاب می‌سازد. یک رکورد می‌تواند همانند هر کلاس دیگری اینترفیس Serializable را پیاده کند. اجزاء قابل‌سریال‌سازی رکورد به‌طور طبیعی برای مواردی مانند ذخیره‌سازی پیکربندی، بازگرداندن وضعیت، ارسال داده‌ها در شبکه یا کش‌کردن مقادیر مناسب هستند.

از آنجا که فیلدهای رکورد نهایی و غیرقابل تغییر هستند، به شما کمک می‌کنند تا از مسائلی که ممکن است هنگام تغییر وضعیت قابل‌تغییر بین سریال‌سازی و دی‌سریال‌سازی پیش آید، جلوگیری کنید. برای مثال:

import java.io.Serializable; record User(String username, int age, Profile profile) implements Serializable {} class Profile { private String bio; }

در اینجا، String و int مشکلی ندارند، اما Profile قابلیت سریال‌سازی ندارد، به این معنی که User نمی‌تواند سریال‌سازی شود. اگر Profile را نیز برای پیاده‌سازی Serializable به‌روزرسانی کنید، User به‌طور کامل قابل‌سریال‌سازی خواهد شد:

class Profile implements Serializable { private String bio; }

فراتر از مبانی سریال‌سازی، پشتیبانی اکوسیستم جاوا از رکوردها به‌سرعت پیشرفت کرده است. چارچوب‌های معروفی مانند Spring Boot، Quarkus و Jackson همه به‌صورت یکپارچه با رکوردها کار می‌کنند، همانند اکثر ابزارهای تست.

به‌دلیل این پذیرش، رکوردها به‌عنوان DTOها در APIهای دنیای واقعی درخشیده‌اند:

@RestController @RequestMapping("/api/orders") public class OrderController { @GetMapping("/{id}") public OrderView getOrder(@PathVariable UUID id) { // In a real app, this would come from a database or service return new OrderView( id, "Duke", List.of(new ItemView(UUID.randomUUID(), 2)), new BigDecimal("149.99") ); } // Record DTOs for API response record OrderView(UUID id, String customerName, List<ItemView> items, BigDecimal total) {} record ItemView(UUID productId, int quantity) {} }

امروزه، اکثر کتابخانه‌ها و ابزارهای اصلی جاوا رکوردها را به‌عنوان شهروندان درجه‌یک می‌شناسند. تردید اولیه به‌طور عمده ناپدید شده است و توسعه‌دهندگان آن‌ها را به‌دلیل وضوح و ایمنیشان می‌پذیرند.

نتیجه‌گیری

رکوردها نمایانگر پیشرفت بنیادی در تکامل جاوا هستند. آن‌ها verbosity کلاس‌های داده را کاهش می‌دهند و عدم تغییرپذیری و رفتار سازگار را تضمین می‌کنند. با حذف کدهای تکراری برای سازنده‌ها، دسترسی‌گرها و متدهایی مانند equals() و hashCode()، رکوردها کد را تمیزتر، بیانگرتر و همگام با شیوه‌های مدرن می‌سازند در حالی که ایمنی نوع را حفظ می‌کنند.

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

با پیشرفت‌هایی مانند رکوردها، کلاس‌های sealed و مطابقت الگو، جاوا به‌تدریج به سمت سبک‌تری متمرکز بر داده‌ها حرکت می‌کند. یادگیری استفاده از این ابزارها یکی از واضح‌ترین راه‌ها برای نوشتن جاوای مدرن و بیانگر است.