Is “final” final at runtime?

2019-04-03 17:23发布

问题:

I've been toying with ASM, and I believe I succeeded in adding the final modifier to an instance field of a class; however I then proceeded to instantiate said class and invoke a setter on it, which successfully changed the value of the now-final field. Am I doing something wrong with my bytecode changes, or is final enforced only by the Java compiler?

Update: (31 Jul) Here's some code for you. The main parts are

  1. a simple POJO with a private int x and private final int y,
  2. the MakeFieldsFinalClassAdapter, which makes every field it visits final unless it already is,
  3. and the AddSetYMethodVisitor, which causes the setX() method of the POJO to also set y to the same value it set x to.

In other words, we start with a class with one final (x) and one non-final (y) field. We make x final. We make setX() set y in addition to setting x. We run. Both x and y get set with no errors. The code is on github. You can clone it with:

git clone git://github.com/zzantozz/testbed.git tmp
cd tmp/asm-playground

Two things of note: The reason I asked this question in the first place: both a field that I made final and a field that was already final are able to be set with what I believe to be normal bytecode instructions.

Another update: (1 Aug) Tested with both 1.6.0_26-b03 and 1.7.0-b147 with the same results. That is, the JVM happily modifies final fields at runtime.

Final(?) update: (19 Sep) I'm removing the full source from this post because it was rather lengthy, but it's still available on github (see above).

I believe I've conclusively proven the JDK7 JVM is in violation of the specification. (See the excerpt in Stephen's answer.) After using ASM to modify bytecode as described previously, I wrote it back out to a class file. Using the excellent JD-GUI, this class file decompiles to the following code:

package rds.asm;

import java.io.PrintStream;

public class TestPojo
{
  private final int x;
  private final int y;

  public TestPojo(int x)
  {
    this.x = x;
    this.y = 1;
  }

  public int getX() {
    return this.x;
  }

  public void setX(int x) {
    System.out.println("Inside setX()");
    this.x = x; this.y = x;
  }

  public String toString()
  {
    return "TestPojo{x=" +
      this.x +
      ", y=" + this.y +
      '}';
  }

  public static void main(String[] args) {
    TestPojo pojo = new TestPojo(10);
    System.out.println(pojo);
    pojo.setX(42);
    System.out.println(pojo);
  }
}

A brief glance at that should tell you that class will never compile due to reassigning a final field, and yet running that class in plain vanilla JDK 6 or 7 looks like this:

$ java rds.asm.TestPojo
TestPojo{x=10, y=1}
Inside setX()
TestPojo{x=42, y=42}
  1. Does anyone else have input before I report a bug on this?
  2. Can anyone confirm whether this should be a bug in JDK 6 or only in 7?

回答1:

Is “final” final at runtime?

Not in the sense you mean.

AFAIK, the semantics of the final modifier are only enforced by the bytecode compiler.

There are no special bytecodes for initializing final fields, and the bytecode verifier (apparently) doesn't check for "illegal" assignments either.

However, the JIT compiler might treat the final modifier as a hint that things don't need to be refetched. So, if your bytecodes modify a variable marked as final you are liable to cause unpredictable behavior. (And the same thing can happen if you use reflection to modify a final variable. The spec clearly says so ...)

And, of course, you can modify a final field using reflection.


UPDATE

I took a look at the Java 7 JVM spec, and it partly contradicts what I said above. Specifically, the description of the PutField opcode says:

"Linking Exceptions ... Otherwise, if the field is final, it must be declared in the current class, and the instruction must occur in an instance initialization method (<init>) of the current class. Otherwise, an IllegalAccessError is thrown.".

So, while you could (in theory) assign to a final field multiple times in the object's constructor, the bytecode verifier should prevent any attempt to load a method that contains bytecode that assign to a final. Which ... when you think about Java security sandboxes ... is a good thing.



回答2:

If the field is final it still may have situation when it is assigned to. For example in constructor. This logic is enforced by compiler as stated in this article. JVM itself would not enforce such rules as performance price would be too high and byte code verifier may not be able to easily determine if field is assigned only once.

So making the field final via ASM, probably does not make much sense.



回答3:

You can overwrite final fields at runtime using reflection. Gson does this all time time while binding JSON to Java objects.