لقد صادفت مؤخرًا موقفًا يستبدل فيه كائن بـ var في برنامج Java 10 استثناءً في وقت التشغيل. أصبحت مهتمًا بعدد الطرق المختلفة لتحقيق هذا التأثير ، وقمت بتوجيه هذه المشكلة إلى المجتمع:
اتضح أنه يمكنك تحقيق التأثير بطرق مختلفة. على الرغم من أن جميعها معقدة بعض الشيء ، إلا أنه من المثير للاهتمام تذكير التفاصيل الدقيقة للغة بمثال هذه المهمة. دعونا نرى ما الأساليب التي تم العثور عليها.
المشاركين
بين المجيبين كان هناك الكثير من الناس وليس الناس جدا. هذا هو سيرجي بيديب إيغوروف ، الموظف المحوري ، المتحدث ، أحد المبدعين في Testcontainers. هذا هو Victor Polishchuk ، المشهور بالتقارير حول المشروع الدامي. لاحظ أيضا نيكيتا Artyushov من جوجل. ديمتري ميخائيلوف وماكيمو . لكنني كنت سعيدًا بشكل خاص بوصول Wouter Coekaerts . كان معروفًا بمقاله في العام الماضي ، حيث تجول عبر نظام نوع جافا وأخبره كيف تم كسره بشكل يائس. بعض هذه المقالة jbaruch وأنا استخدمها حتى في الإصدار الرابع من Java Puzzlers .
المهمة والحلول
لذلك ، فإن جوهر مهمتنا هو: هناك برنامج Java حيث يوجد إعلان عن متغير النموذج Object x = ...
( java.lang.Object
قياسي صادق ، لا توجد بدائل نوع). البرنامج يجمع ويدير ويطبع شيئا مثل "موافق". نحن نستبدل Object
بـ var
، نحتاج إلى الاستدلال التلقائي على الكتابة ، وبعد ذلك يستمر البرنامج في الترجمة ، لكنه يتعطل عند بدء التشغيل باستثناء.
يمكن تقسيم الحلول تقريبًا إلى مجموعتين. في البداية ، بعد الاستبدال بـ var ، يصبح المتغير بدائيًا (أي أنه كان في الأصل autoboxing). يبقى النوع الثاني كائنًا ، ولكنه أكثر تحديدًا من Object
. هنا يمكنك تسليط الضوء على مجموعة فرعية مثيرة للاهتمام والتي تستخدم الأدوية الجنيسة.
الملاكمة
كيف نميز كائن عن بدائي؟ هناك العديد من الطرق المختلفة. الأسهل هو التحقق من الهوية. اقترح هذا الحل نيكيتا:
Object x = 1000; if (x == new Integer(1000)) throw new Error(); System.out.println("Ok");
عندما يكون x
كائنًا ، فمن المؤكد أنه لا يمكن أن يكون مساويًا بالرجوع إلى الكائن new Integer(1000)
. وإذا كانت بدائية ، فطبقًا لقواعد اللغة ، تتحول new Integer(1000)
فورًا إلى بدائية أيضًا ، وتتم مقارنة الأرقام بالبدائية.
طريقة أخرى هي طرق طاقتها. يمكنك كتابة كتابك الخاص ، ولكن جاء سيرجي بخيار أكثر أناقة: استخدم المكتبة القياسية. طريقة List.remove
هي List.remove
، والتي تم تحميلها بشكل زائد ويمكن أن تزيل إما عنصرًا بفهرس إذا تم تمرير بدائي ، أو عنصر حسب القيمة إذا تم تمرير كائن. لقد أدى هذا مرارًا وتكرارًا إلى حدوث خلل في البرامج الحقيقية إذا كنت تستخدم List<Integer>
. لمهمتنا ، قد يبدو الحل كما يلي:
Object x = 1000; List<?> list = new ArrayList<>(); list.remove(x); System.out.println("Ok");
نحن نحاول الآن إزالة العنصر 1000 غير الموجود من القائمة الفارغة ، وهذا مجرد إجراء عديم الفائدة. ولكن إذا استبدلنا Object
بـ var
، var
طريقة أخرى تزيل العنصر بالفهرس 1000. وهذا يؤدي بالفعل إلى IndexOutOfBoundsException
.
الطريقة الثالثة هي مشغل تحويل النوع. يمكننا تحويل بدائي آخر بنجاح إلى نوع بدائي ، ولكن لا يتم تحويل كائن إلا إذا كان هناك غلاف على نفس النوع الذي سنحول إليه (ثم ستحدث علبة الوارد). في الواقع ، نحن بحاجة إلى التأثير المعاكس: يوجد استثناء في حالة بدائية ، وليس في حالة كائن ، ولكن باستخدام تجربة التجريب هذا أمر سهل ، والذي استخدمه Viktor:
Object x = 40; try { throw new Error("Oops :" + (char)x); } catch (ClassCastException e) { System.out.println("Ok"); }
هنا ، ClassCastException
هو السلوك المتوقع ، ثم يخرج البرنامج بشكل طبيعي. ولكن بعد استخدام var
يختفي هذا الاستثناء ، ونلقي شيئًا آخر. أتساءل ما إذا كان هذا مستوحى من الكود الحقيقي من المؤسسة الدموية؟
تم اقتراح خيار تحويل نوع آخر بواسطة Wouter. يمكنك استخدام المنطق الغريب للمشغل ?:
. صحيح أن الكود الخاص به يعطي نتائج مختلفة فقط ، لذلك عليك تعديله بطريقة ما بحيث يكون هناك استثناء. لذلك ، يبدو لي ، أناقة للغاية:
Object x = 1.0; System.out.println(String.valueOf(false ? x : 100000000000L).substring(12) + "Ok");
الفرق بين هذه الطريقة هو أننا لا نستخدم قيمة x
مباشرة ، لكن النوع x
يؤثر على نوع التعبير false ? x : 100000000000L
false ? x : 100000000000L
. إذا كانت x
عبارة عن Object
، فإن نوع التعبير بأكمله هو Object
، ثم لدينا الملاكمة فقط ، String.valueOf()
سلسلة من 100000000000
، والتي substring(12)
هي سلسلة فارغة. إذا كنت تستخدم var
، يصبح النوع x
double
، وبالتالي النوع false ? x : 100000000000L
false ? x : 100000000000L
double
أيضًا ، أي ، 100000000000L
ستتحول إلى 1.0E11
، حيث تقل عن 12 حرفًا ، لذا يؤدي استدعاء substring
إلى StringIndexOutOfBoundsException
.
أخيرًا ، نستفيد من حقيقة أنه يمكن تغيير المتغير بالفعل بعد الإنشاء. وفي متغير الكائن ، على عكس البدائي ، يمكنك وضع null
. من السهل وضع null
في متغير ؛ فهناك العديد من الطرق. ولكن هنا ، اتخذ Wouter أيضًا مقاربة إبداعية باستخدام طريقة Integer.getInteger
السخيفة:
Object x = 1; x = Integer.getInteger("moo"); System.out.println("Ok");
لا يعلم الجميع أن هذه الطريقة تقرأ خاصية نظام تسمى moo
، وإذا كان هناك واحدًا ، فإنها تحاول تحويلها إلى رقم ، وإلا فإنها تُرجع. إذا لم تكن هناك خاصية ، فنحن نعيِّن بهدوء الكائن بهدوء ، ولكننا نقع من NullPointerException
عند محاولة تعيينه إلى بدائي (يحدث وضع تلقائي للبوكسينج). كان يمكن أن يكون أسهل ، بالطبع. إصدار تقريبي x = null;
لا يتم الزحف إليه - لا يتم التحويل إليه ، ولكن المحول البرمجي سيبتلعه الآن:
Object x = 1; x = (Integer)null; System.out.println("Ok");
نوع الكائن
لنفترض أنه لم يعد بإمكانك اللعب مع البدائيين. ماذا يمكنك أن تفكر في؟
حسنًا ، أولاً ، أبسط طريقة التحميل الزائد التي اقترحها ديمتري:
public static void main(String[] args) { Object x = "Ok"; sayWhat(x); } static void sayWhat(Object x) { System.out.println(x); } static void sayWhat(String x) { throw new Error(); }
يحدث الربط بين الطرق المثقلة في Java بشكل ثابت ، في مرحلة الترجمة. يتم استدعاء الأسلوب sayWhat sayWhat(Object)
، ولكن إذا استنتجنا النوع x
تلقائيًا ، فسيتم sayWhat(String)
String
، وبالتالي سيتم ربط طريقة sayWhat(String)
الأكثر تحديدًا.
هناك طريقة أخرى لإجراء مكالمة غامضة في Java باستخدام الوسائط المتغيرة (varargs). وتذكر ووتر هذا مرة أخرى:
Object x = new Object[] {}; Arrays.asList(x).get(0); System.out.println("Ok");
عندما يكون نوع المتغير Object
، يعتقد المحول البرمجي أنه وسيطة متغيرة ويلف الصفيف في صفيف آخر من عنصر واحد ، لذلك ينجز get()
بنجاح. إذا كنت تستخدم var
، فسيتم عرض النوع Object[]
، ولن يكون هناك غلاف إضافي. بهذه الطريقة نحصل على قائمة فارغة ، وسوف تفشل استدعاء get()
.
ذهب ماكيمو إلى فاضح: قرر استدعاء println من خلال واجهة برمجة تطبيقات MethodHandle:
Object x = "Ok"; MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual( PrintStream.class, "println", MethodType.methodType(void.class, Object.class)); mh.invokeExact(System.out, x);
invokeExact
طريقة invokeExact
وعدة طرق أخرى من java.lang.invoke
على ما يسمى "التوقيع متعدد الأشكال". على الرغم من أنه تم التصريح كأسلوب vararg invokeExact(Object... args)
المعتاد ، فإنه لا يحدث في التعبئة الصفيف القياسية. بدلاً من ذلك ، يتم إنشاء توقيع في الرمز الفرعي الذي يطابق أنواع الوسائط التي تم تمريرها بالفعل. تم invokeExact
طريقة invokeExact
لاستدعاء مقابض الطريقة بسرعة فائقة ، بحيث لا تقوم بأي تحويلات وسيطة قياسية مثل الصب أو الملاكمة. من المتوقع أن يتطابق نوع طريقة المقبض تمامًا مع توقيع المكالمة. يتم التحقق من ذلك في وقت التشغيل ، وكما هو الحال في حالة كسر المطابقة ، نحصل على WrongMethodTypeException
.
الأدوية
بالطبع ، يمكن لمعلمات الأنواع إضافة وميض إلى أي مهمة في Java. جلبت ديمتري حلا مشابها للرمز الذي صادفته في البداية. قرار ديمتري مطول ، لذا سأعرض:
public static void main(String[] args) { Object x = foo(new StringBuilder()); System.out.println(x); } static <T> T foo(T x) { return (T)"Ok"; }
النوع T
هو الإخراج كـ StringBuilder
، ولكن في هذا الرمز ، لا يُطلب من المحول البرمجي إدراج تدقيق نوع في نظير الطلب في الرمز الفرعي. يكفي بالنسبة له أنه يمكن تعيين StringBuilder
إلى Object
، مما يعني أن كل شيء على ما يرام. لا يوجد أحد ضد حقيقة أن الطريقة التي لها قيمة الإرجاع StringBuilder
بإرجاع السلسلة بالفعل إذا قمت بتعيين النتيجة لمتغير من النوع Object
أي حال. يحذر المترجم بأمانة أن لديك فريق عمل غير مراقب ، مما يعني أنه يغسل يديه. ومع ذلك ، عند استبدال x
بـ var
يتم عرض type x
أيضًا كـ StringBuilder
، ولم يعد ممكناً دون التحقق من الكتابة ، لأن تعيين شيء آخر لمتغير type StringBuilder
لا قيمة له. نتيجة لذلك ، بعد التغيير إلى var
يتعطل البرنامج بأمان مع ClassCastException
.
اقترح Wouter البديل من هذا الحل باستخدام الطرق القياسية:
Object o = ((List<String>)(List)List.of(1)).get(0); System.out.println("Ok");
أخيرًا ، هناك خيار آخر من Wouter:
Object x = ""; TreeSet<?> set = Stream.of(x) .collect(toCollection(() -> new TreeSet<>((a, b) -> 0))); if (set.contains(1)) { System.out.println("Ok"); }
هنا ، اعتمادًا على استخدام var
أو Object
يتم عرض نوع الدفق إما Stream<Object>
أو Stream<String>
. وفقا لذلك ، يتم TreeSet
نوع TreeSet
ونوع lambda المقارنة. في حالة var
، يجب أن تأتي السلاسل إلى lambda ، لذلك عند إنشاء تمثيل وقت التشغيل lambda ، يتم إدراج تحويل نوع تلقائيًا ، والذي يعطي ClassCastException
عند محاولة ClassCastException
وحدة إلى سلسلة.
بشكل عام ، كانت النتيجة مملة للغاية. إذا استطعت إيجاد طرق مختلفة اختلافًا أساسيًا لكسر var
، فاكتب في التعليقات.