Saturday, February 27, 2010

Final field in Java: is it really?

import java.lang.reflect.Field;

public class ItsFinallyTrue {
   
   public final boolean dream1 = false;
   public final Boolean dream2 = false;
   
   @Override public String toString() {
      return dream1 + "-" + dream2;
   }

   public static void main(String[] args)
         throws IllegalAccessException, SecurityException {

      ItsFinallyTrue dreamer = new ItsFinallyTrue();
      System.out.println(dreamer);
      
      for (Field dream : dreamer.getClass().getDeclaredFields()) {
         dream.setAccessible(true);
         dream.set(dreamer, true);
      }      
      System.out.println(dreamer);
   }
}
What will happen if you run this code?

(A) It throws IllegalAccessException
(B) It throws SecurityException
(C) It prints false-false and false-false
(D) It prints false-false and false-true
(E) It prints false-false and true-true

(A) seems to make sense, since using reflection to set a final field seems well-qualified for an "illegal access". In fact, this is exactly the behavior specified in the documentation (Field.set @throws IllegalAccessException), except for one catch:
If the underlying field is final, the method throws an IllegalAccessException unless setAccessible(true) has succeeded for this field and this field is non-static.
A-ha! So apparently under certain condition it is possible to set a final field through reflection!

(B) is now a possibility, since AccessibleObject.setAccessible @throws SecurityException. In fact, under especially strict SecurityManager policies, this is the correct answer. Of course, this isn't all that interesting, so let's consider the rest of the possibilities.

(C), (D), (E) are now possible. (C) is perhaps motivated by the idea that while no exception is thrown, the fields themselves don't actually get new values (since they are final after all!). (E) is the opposite: if exceptions aren't thrown, then everything is lemonade and sunshine and the fields must get new values. (D) just sits somewhere in between the two extremes.

In fact, of the three, (D) is the correct answer. It is in fact true that both fields get assigned new values, but dream1 is a final primitive, which makes its value an inlinable compile-time constant; the toString method doesn't actually fetch the value of that field.

This is evident in the bytecodes (edited for clarity), which skips even the boolean primitive to String conversion:
public java.lang.String toString();
 0: new           #32; //class StringBuilder
 3: dup
 4: ldc           #34; //String "false-"
 6: invokespecial #36; //Constructor StringBuilder(String)
 9: aload_0
10: getfield      #24; //Field Boolean dream2
13: invokevirtual #39; //Method StringBuilder StringBuilder.append(Object)
16: invokevirtual #43; //Method String StringBuilder.toString()
19: areturn
That is, the bytecodes, when reverse-engineered back to Java, actually does something like this:
return new StringBuilder("false-").append(this.dream2).toString();

No comments:

Post a Comment