تعديل الحقول النهائية في جافا
-
06-07-2019 - |
سؤال
لنبدأ بحالة اختبار بسيطة:
import java.lang.reflect.Field;
public class Test {
private final int primitiveInt = 42;
private final Integer wrappedInt = 42;
private final String stringValue = "42";
public int getPrimitiveInt() { return this.primitiveInt; }
public int getWrappedInt() { return this.wrappedInt; }
public String getStringValue() { return this.stringValue; }
public void changeField(String name, Object value) throws IllegalAccessException, NoSuchFieldException {
Field field = Test.class.getDeclaredField(name);
field.setAccessible(true);
field.set(this, value);
System.out.println("reflection: " + name + " = " + field.get(this));
}
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
Test test = new Test();
test.changeField("primitiveInt", 84);
System.out.println("direct: primitiveInt = " + test.getPrimitiveInt());
test.changeField("wrappedInt", 84);
System.out.println("direct: wrappedInt = " + test.getWrappedInt());
test.changeField("stringValue", "84");
System.out.println("direct: stringValue = " + test.getStringValue());
}
}
أي شخص يهتم بتخمين ما سيتم طباعته كإخراج (كما هو موضح في الأسفل حتى لا يفسد المفاجأة على الفور).
الأسئلة هي:
- لماذا يتصرف العدد الصحيح البدائي والمغلف بشكل مختلف؟
- لماذا يُرجع الوصول الانعكاسي مقابل الوصول المباشر نتائج مختلفة؟
- أكثر ما يزعجني هو لماذا تتصرف السلسلة مثل البدائية
int
وليس مثلInteger
?
النتائج (جافا 1.5):
reflection: primitiveInt = 84
direct: primitiveInt = 42
reflection: wrappedInt = 84
direct: wrappedInt = 84
reflection: stringValue = 84
direct: stringValue = 42
المحلول
يتم تضمين ثوابت وقت الترجمة (في وقت الترجمة javac).راجع JLS، على وجه الخصوص 15.28 يحدد تعبيرًا ثابتًا ويناقش 13.4.9 التوافق الثنائي أو الحقول والثوابت النهائية.
إذا جعلت الحقل غير نهائي أو قمت بتعيين ثابت وقت غير مترجم، فلن يتم تضمين القيمة.على سبيل المثال:
سلسلة نهائية خاصة stringValue = null!=null؟"":"42" ؛
نصائح أخرى
وهذا في رأيي أسوأ من ذلك:وأشار أحد الزملاء إلى الأمر المضحك التالي:
@Test public void testInteger() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field value = Integer.class.getDeclaredField("value");
value.setAccessible(true);
Integer manipulatedInt = Integer.valueOf(7);
value.setInt(manipulatedInt, 666);
Integer testInt = Integer.valueOf(7);
System.out.println(testInt.toString());
}
من خلال القيام بذلك، يمكنك تغيير سلوك JVM بأكمله الذي تعمل فيه.(بالطبع يمكنك تغيير القيم بين -127 و127 فقط)
خواطر set(..)
تعمل الطريقة مع FieldAccessor
س.
ل int
يحصل على UnsafeQualifiedIntegerFieldAccessorImpl
, ، الذي تحدد فئته الفائقة readOnly
الخاصية تكون صحيحة فقط إذا كان الحقل كلاهما static
و final
لذا، للإجابة أولاً على السؤال غير المطروح - إليك السبب final
يتم تغييره دون استثناء.
جميع الفئات الفرعية من UnsafeQualifiedFieldAccessor
استخدم ال sun.misc.Unsafe
الطبقة للحصول على القيم.الأساليب هناك كلها native
, ، لكن أسمائهم getVolatileInt(..)
و getInt(..)
(getVolatileObject(..)
و getObject(..)
على التوالى).تستخدم الملحقات المذكورة أعلاه الإصدار "المتقلب".إليك ما يحدث إذا أضفنا الإصدار غير المتطاير:
System.out.println("reflection: non-volatile primitiveInt = "
unsafe.getInt(test, (long) unsafe.fieldOffset(getField("primitiveInt"))));
(أين unsafe
is instantiated by reflection - it is not allowed otherwise)
(and I call getObject
ل Integer
و String
)
وهذا يعطي بعض النتائج المثيرة للاهتمام:
reflection: primitiveInt = 84
direct: primitiveInt = 42
reflection: non-volatile primitiveInt = 84
reflection: wrappedInt = 84
direct: wrappedInt = 84
reflection: non-volatile wrappedInt = 84
reflection: stringValue = 84
direct: stringValue = 42
reflection: non-volatile stringValue = 84
في هذه المرحلة أتذكر مقال في javaspecialists.eu مناقشة مسألة ذات صلة.يقتبس جي إس آر-133:
إذا تمت تهيئة الحقل النهائي إلى ثابت وقت الترجمة في إعلان الحقل، فقد لا يتم ملاحظة التغييرات في الحقل النهائي، حيث يتم استبدال استخدامات هذا الحقل النهائي في وقت الترجمة بثابت وقت الترجمة.
ويناقش الفصل التاسع التفاصيل التي لوحظت في هذا السؤال.
واتضح أن هذا السلوك ليس غير متوقع، منذ التعديلات final
من المفترض أن تحدث الحقول مباشرة بعد تهيئة الكائن.
وهذا ليس جوابا، ولكنه يثير نقطة أخرى من الارتباك:
كنت أرغب في معرفة ما إذا كانت المشكلة تتعلق بتقييم وقت الترجمة أو ما إذا كان الانعكاس يسمح بالفعل لـ Java بالتغلب على final
الكلمة الرئيسية.هنا برنامج اختبار.كل ما أضفته هو مجموعة أخرى من مكالمات getter، لذا هناك واحدة قبل كل منها وبعدها changeField()
يتصل.
package com.example.gotchas;
import java.lang.reflect.Field;
public class MostlyFinal {
private final int primitiveInt = 42;
private final Integer wrappedInt = 42;
private final String stringValue = "42";
public int getPrimitiveInt() { return this.primitiveInt; }
public int getWrappedInt() { return this.wrappedInt; }
public String getStringValue() { return this.stringValue; }
public void changeField(String name, Object value) throws IllegalAccessException, NoSuchFieldException {
Field field = MostlyFinal.class.getDeclaredField(name);
field.setAccessible(true);
field.set(this, value);
System.out.println("reflection: " + name + " = " + field.get(this));
}
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
MostlyFinal test = new MostlyFinal();
System.out.println("direct: primitiveInt = " + test.getPrimitiveInt());
test.changeField("primitiveInt", 84);
System.out.println("direct: primitiveInt = " + test.getPrimitiveInt());
System.out.println();
System.out.println("direct: wrappedInt = " + test.getWrappedInt());
test.changeField("wrappedInt", 84);
System.out.println("direct: wrappedInt = " + test.getWrappedInt());
System.out.println();
System.out.println("direct: stringValue = " + test.getStringValue());
test.changeField("stringValue", "84");
System.out.println("direct: stringValue = " + test.getStringValue());
}
}
هذا هو الناتج الذي أحصل عليه (ضمن Eclipse، Java 1.6)
direct: primitiveInt = 42
reflection: primitiveInt = 84
direct: primitiveInt = 42
direct: wrappedInt = 42
reflection: wrappedInt = 84
direct: wrappedInt = 84
direct: stringValue = 42
reflection: stringValue = 84
direct: stringValue = 42
لماذا يتغير الاتصال المباشر بـ getWrappedInt()؟
هناك عمل حول هذا.إذا قمت بتعيين قيمة الملف النهائي الثابت الخاص الذي تم تقديمه في الكتلة الثابتة {}، فسوف يعمل لأنه لن يتم تضمين الحقل:
private static final String MY_FIELD;
static {
MY_FIELD = "SomeText"
}
...
Field field = VisitorId.class.getDeclaredField("MY_FIELD");
field.setAccessible(true);
field.set(field, "fakeText");