Optional وجافا الحديثة

instanceof المحسّن وأنماط السجلّات

15 دقيقة الدرس 9 من 13

instanceof المحسّن وأنماط السجلّات

قبل Java 16، كان التحقّق من نوع كائن ثم استخدامه بذلك النوع يستلزم خطوتين منفصلتين: فحص instanceof ثم تحويل صريح (cast). قدّمت Java 16 مطابقة الأنماط لـ instanceof، وامتدّت Java 21 لتشمل أنماط السجلّات — ممّا يتيح تفكيك مكوّنات السجلّ مباشرةً داخل النمط. معًا تُزيل هذه الميزات فئةً من الشيفرة الزائدة عن الحاجة موجودة في Java منذ إصدارها الأوّل.

الطريقة القديمة: الفحص ثم التحويل

كتب كلّ مطوّر Java شيفرةً كهذه في مرحلة ما:

Object obj = getShape(); if (obj instanceof Circle) { Circle c = (Circle) obj; // تحويل زائد — نعرف بالفعل أنه Circle System.out.println("Radius: " + c.radius()); }

التحويل في السطر الثاني مجرّد إجراء شكلي. يعلم المُجمِّع بالفعل أنّ النوع صحيح لأنّ فحص instanceof نجح. الكاست موجود فقط لأنّ اللغة كانت تستلزمه.

مطابقة الأنماط لـ instanceof

مع مطابقة الأنماط تجمع الفحص والربط في تعبير واحد:

Object obj = getShape(); if (obj instanceof Circle c) { // c مُكتَّبة بالفعل كـ Circle — لا حاجة لتحويل System.out.println("Radius: " + c.radius()); }

متغيّر النمط c يكون في النطاق فقط حيث تكون المطابقة مضمونة. يُطبّق المُجمِّع هذا القيد: لا يمكنك استخدام c عن طريق الخطأ في فرع لم تنجح فيه المطابقة.

النطاق والتعيين المحدد: متغيّر النمط يكون في النطاق فقط في الفرع الصحيح (true) من if، وليس في فرع else وبالتأكيد ليس خارج التعبير. يرفض المُجمِّع أيّ محاولة لاستخدامه حيث لا تكون المطابقة مضمونة.

استخدام متغيّر النمط في الشروط

يمكن أن تظهر متغيّرات النمط في نفس التعبير المنطقي، ممّا يتيح حراسات مدمجة:

// يطابق فقط السلاسل غير الفارغة ذات الطول > 5 if (obj instanceof String s && s.length() > 5) { System.out.println("Long string: " + s); }

لأنّ Java تقصير-تحقّق (short-circuit) مع &&، فإنّ الجانب الأيمن من المُشغّل يُصل إليه فقط عند نجاح المطابقة، ممّا يضمن أنّ s هي String هناك. المنطق ذاته يعني أنّ || لن تكون آمنة — ويرفضها المُجمِّع.

مطابقة الأنماط في تعبيرات Switch (Java 21)

عمّمت Java 21 الأنماط على switch. يمكنك المطابقة على النوع وربط متغيّر وإضافة حارسة when في ذراع واحدة:

sealed interface Shape permits Circle, Rectangle, Triangle {} record Circle(double radius) implements Shape {} record Rectangle(double width, double height) implements Shape {} record Triangle(double base, double height) implements Shape {} double area(Shape shape) { return switch (shape) { case Circle c -> Math.PI * c.radius() * c.radius(); case Rectangle r -> r.width() * r.height(); case Triangle t -> 0.5 * t.base() * t.height(); }; }

لأنّ Shape مغلقة (sealed)، يعرف المُجمِّع أنّ الأنواع الفرعية الثلاثة المسموح بها تُغطّي كل الحالات ولا يتطلّب ذراع default. إضافة نوع فرعي رابع لاحقًا ستُحوّل الذراع المفقودة إلى خطأ تصريفي — لا إخفاق صامت وقت التشغيل.

حارسات when

تُكمّل جملة when ذراع النمط بتعبير منطقي اختياري:

String describe(Object obj) { return switch (obj) { case Integer i when i < 0 -> "negative integer: " + i; case Integer i when i == 0 -> "zero"; case Integer i -> "positive integer: " + i; case String s when s.isEmpty() -> "empty string"; case String s -> "string: " + s; default -> "other: " + obj; }; }

تُقيَّم الذراعات من الأعلى إلى الأسفل. تفوز أوّل ذراع يطابق نمط نوعها وتكون حارستها when صحيحة. هذا يجعل الترتيب ذا معنى — على عكس switch التقليدي على الأنواع البدائية، لا يوجد خطر التسقّط (fall-through).

أفضل ممارسة مع حارسات when: ضع الحارسات الأكثر تحديدًا قبل الأقل تحديدًا للنوع ذاته. لا يُنبّه المُجمِّع على الذراعات غير القابلة للوصول بسبب الحارسات (فقط لأنماط الأنواع)، فالترتيب مسؤوليتك.

أنماط السجلّات: التفكيك في مكانه

قدّمت Java 21 أنماط السجلّات التي تتيح مطابقة نوع السجلّ وربط مكوّناته في آنٍ واحد. بدلًا من استدعاء الدوالّ الصلة بعد المطابقة، تُسمّي المكوّنات مباشرةً في النمط:

record Point(int x, int y) {} record Line(Point start, Point end) {} Object obj = new Line(new Point(1, 2), new Point(3, 4)); // بدون أنماط السجلّات if (obj instanceof Line line) { int x1 = line.start().x(); int y1 = line.start().y(); System.out.println("Starts at " + x1 + ", " + y1); } // مع أنماط السجلّات — المكوّنات مربوطة مباشرةً if (obj instanceof Line(Point start, Point end)) { System.out.println("Starts at " + start.x() + ", " + start.y()); System.out.println("Ends at " + end.x() + ", " + end.y()); }

نمط السجلّ Line(Point start, Point end) يختبر في آنٍ واحد أنّ obj هو Line ويفكّكه إلى مكوّنيه في عملية واحدة.

أنماط السجلّات المتداخلة

أنماط السجلّات قابلة للتركيب. يمكنك تداخلها للوصول عميقًا في بنية البيانات دون متغيّرات وسيطة:

// تفكيك متداخل: مطابقة خط بدايته عند الأصل if (obj instanceof Line(Point(int x, int y), Point end) && x == 0 && y == 0) { System.out.println("Line starts at the origin, ends at " + end); }

النمط الداخلي Point(int x, int y) يفكّك المكوّن start. النمط الخارجي يفكّك Line. كلا الربطين في النطاق داخل الجسم.

سلامة null: لا يطابق نمط السجلّ null أبدًا. إذا كان obj هو null، يفشل النمط بالكامل فورًا — دون رمي NullPointerException. هذا متسق مع كل مطابقة أنماط في Java.

أنماط السجلّات في Switch

تبرز أنماط السجلّات أكثر في switch حيث تحتاج إلى تفكيك أشكال متعددة:

sealed interface Expr permits Num, Add, Mul {} record Num(int value) implements Expr {} record Add(Expr left, Expr right) implements Expr {} record Mul(Expr left, Expr right) implements Expr {} int eval(Expr expr) { return switch (expr) { case Num(int v) -> v; case Add(Expr l, Expr r) -> eval(l) + eval(r); case Mul(Expr l, Expr r) -> eval(l) * eval(r); }; } // eval(new Add(new Num(2), new Mul(new Num(3), new Num(4)))) == 14

هذا مُقيَّم تعبير كامل متعاود في سبعة أسطر. تستخرج أنماط السجلّات l وr من عقدتَي Add وMul على نفس السطر الذي يحدّد نوع العقدة — لا متغيّرات وسيطة، لا دوالّ وصول.

المقايضات ومتى تستخدم هذه الميزات

  • استخدم مطابقة الأنماط لـ instanceof كلّما كنت ستكتب فحصًا يليه تحويل. إنّها أفضل بالمطلق: ضوضاء أقل، لا خطر ClassCastException خفي من اسم متغيّر خاطئ.
  • فضّل التسلسلات الهرمية المغلقة + أنماط switch على سلاسل if/else instanceof الطويلة. فحص الاستيعاب الشامل يجعل الشيفرة أكثر متانة وأسهل صيانةً.
  • استخدم أنماط السجلّات عندما تكون بنية البيانات هي محور الاهتمام. تجعل الشيفرة الخوارزمية التي تجتاز البيانات المتعاودة (أشجار التعبير، مُحلِّلات AST، نماذج JSON) أوضح بكثير.
  • لا تُبالغ في التداخل. أنماط السجلّات المتداخلة عميقًا (A(B(C(D d)))) يصعب قراءتها. إذا كانت البنية أعمق من مستويين، فاستخراج متغيّر وسيط أوضح في الغالب.

الخلاصة

تستبدل مطابقة الأنماط لـ instanceof (Java 16) نمط الفحص ثم التحويل بتعبير ربط واحد. تمتدّ أنماط switch (Java 21) لتشمل الإرسال متعدّد الذراعات مع حارسات when وفحوصات الاستيعاب الشامل للأنواع المغلقة. تُضيف أنماط السجلّات (Java 21) التفكيك — ربط مكوّنات السجلّ مباشرةً في النمط. هذه الميزات الثلاث مجتمعةً تجعل الشيفرة المدفوعة بالأنواع في Java موجزةً وآمنةً ومُتحقَّقًا منها بالمُجمِّع، مُقرِّبةً اللغة من قدرات مطابقة الأنماط الموجودة في Haskell وScala وKotlin.