Source Gist @ http://gist.github.com/491163Every so often I run in a Java programmer who uses the final modifier in method parameters in Java. The code usually looks as follows
public void executeOperation(final OperationType ot, final OperationParameters op) {
} I often wonder whether the programmer has a sufficiently clear understanding of the final modifier when using it is such fashion. Given the JVM call by value semantics, there is only a small benefit associated with the additional typing undertaken to add the final modifier on each method parameter. One question you can expect from someone who has used the final modifier without understanding its implications (or lack of) in the context of method parameters involves overriding such methods and the resolution of the same. This question was recently asked on a mailing list I frequent.In the following code snippets
public abstract class Base {
public abstract void methodA(final Type xyz);
}
public class Derived extends Base {
public void methodA(Type xyz) {
// .. code
}
// Or when using interfaces
public interface Interface {
public void methodA(final Type xyz);
}
public class Implementation implements Interface {
public void methodA(Type xyz) {
}
} Why does the compiler not complain, since I am not overriding the method because the signature is not the same.There are obviously a couple of trivial answers, one of which provided in the mailing list is "Whether or not a method parameter is final isn't one of the considerations taken into account when determining whether or not two methods have override-equivalent signature", accompanied by the obligatory reference to JLS 8.4.2 on Method Signatures. The answer adds to my chagrin, not only is it not a good answer it takes a good learning opportunity and converts it into a rote reference to printed material already available on the internet. This was the answer provided to the question on the mailing list and to my surprise was accepted by the original post.
What then would be a sufficiently good answer? In the remainder of this post I set out my stall and attempt to address the original question. The goal it explain the reason for why the final modifier is not considered part of the method signature.
One reason that I see increasingly more programmers using the final modifier on method parameter is the proliferation of programming languages on the JVM and the increasing extent of the use of multi core processors it is fanciful to try to bring the benefits of immutable state programming from the newer languages to Java. Programmers without a good understanding of any of the languages or the JVM mechanisms use final method modifiers excessively.
Tho, the number of languages on the JVM it is worth recalling the the Java language and the JVM was developed at the same time, and that the JVM was developed primarily for execution of Java sources. As a consequence the JLS specification was constrained by implementation choices on the JVM and vice versa. There was no intention of hosting other languages on the JVM. This situation is currently changing with the upcoming Java 7 release of the language and JVM with support for closures and method handles in the language and the JVM respectively.
What then prevent the JLS to enforce final annotation when resolving overridden method? The JVM obviously, has no knowledge of final modifiers on method parameters, they are just language syntactic sugar. We will demonstrate this using the following code, which uses ASM to perform byte code manipulation. (Explanation of the important snippets follows immediately after)
// FILE: FinalVariableTransformer.java
package c.a;
import static org.objectweb.asm.Opcodes.BIPUSH;
import static org.objectweb.asm.Opcodes.ILOAD;
import static org.objectweb.asm.Opcodes.IMUL;
import static org.objectweb.asm.Opcodes.ISTORE;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodAdapter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
class InsertInstructionMethodAdapter extends MethodAdapter {
public InsertInstructionMethodAdapter(MethodVisitor mv) {
super(mv);
}
@Override
public void visitVarInsn(int opcode, int var) {
if (opcode == Opcodes.ILOAD && var == 1) {
// MULTIPLY VAR 1 (i) BY 2
mv.visitVarInsn(ILOAD, 1);
mv.visitIntInsn(BIPUSH, 2);
mv.visitInsn(IMUL);
// STORE VAR 1 (i)
mv.visitVarInsn(ISTORE, 1);
}
super.visitVarInsn(opcode, var);
}
}
class InsertInstructionClassAdapter extends ClassAdapter {
public InsertInstructionClassAdapter(ClassVisitor cv) {
super(cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature,
exceptions);
if ("timesTen".equals(name))
return new InsertInstructionMethodAdapter(mv);
return mv;
}
}
class InsertInstructionTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
return transform(classfileBuffer);
}
public byte[] transform(byte[] classfileBuffer) {
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
ClassAdapter ca = new InsertInstructionClassAdapter(cw);
cr.accept(ca, 0);
return cw.toByteArray();
}
public void transform(String source, String target) throws IOException {
writeFileAsBytes(new File(target), transform(getFileAsBytes(new File(
source))));
}
public void writeFileAsBytes(File file, byte[] bytes) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(file));
bos.write(bytes);
bos.close();
}
private final byte[] getFileAsBytes(final File file) throws IOException {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(
file));
byte[] bytes = new byte[(int) file.length()];
bis.read(bytes);
bis.close();
return bytes;
}
}
public class FinalVariableTransformer {
public static void premain(String agentArguments,
Instrumentation instrumentation) {
instrumentation.addTransformer(new InsertInstructionTransformer());
}
public static void main(String[] args) throws IOException {
if (args.length < 2) {
System.out.println("Require source and target");
return;
}
InsertInstructionTransformer transformer = new InsertInstructionTransformer();
transformer.transform(args[0], args[1]);
}
}
// FILE META-INF/MANIFEST.MF
Manifest-Version: 1.0
Premain-Class: c.a.FinalVariableTransformer
// CLIENT CLASS
public class Simple {
// modify this to be timesTwenty
public int timesTen(final int i) {
return i * 10;
}
public static void main(String[] args) {
Simple s = new Simple();
System.out.println(s.timesTen(2));
}
}
// ON THE COMMAND LINE RUN WITH
java -javaagent:FinalVariableTransformerPackagedAsjar Simple
// AND COMPARE RESULTS WITH JAVAP
That is quite a lot of code, let up take it down in small pieces. The following is the Manifest entry for the premain class when our code is packaged as a jar
// FILE META-INF/MANIFEST.MF
Manifest-Version: 1.0
Premain-Class: c.a.FinalVariableTransformer
The presence of the Premain-Class should indicate to the reader that the class provides a premain method for class instrumentation, using the java.lang.Instrumenation [2] API's. The class FinalVariableTransformer is defined as such
public class FinalVariableTransformer {
public static void premain(String agentArguments,
Instrumentation instrumentation) {
instrumentation.addTransformer(new InsertInstructionTransformer());
}
public static void main(String[] args) throws IOException {
if (args.length < 2) {
System.out.println("Require source and target");
return;
}
InsertInstructionTransformer transformer = new InsertInstructionTransformer();
transformer.transform(args[0], args[1]);
}
}We implement the method premain with the signature as defined by the API's. We instantiate an instance of InsertInstructionTransformer and add it to the chain of applicable transformations by invoking add on the Instrumentation instance method parameter to premain. InsertInstructionTransformer is defined as below
class InsertInstructionTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
return transform(classfileBuffer);
}
public byte[] transform(byte[] classfileBuffer) {
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
ClassAdapter ca = new InsertInstructionClassAdapter(cw);
cr.accept(ca, 0);
return cw.toByteArray();
}
public void transform(String source, String target) throws IOException {
writeFileAsBytes(new File(target), transform(getFileAsBytes(new File(
source))));
}
public void writeFileAsBytes(File file, byte[] bytes) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(file));
bos.write(bytes);
bos.close();
}
private final byte[] getFileAsBytes(final File file) throws IOException {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(
file));
byte[] bytes = new byte[(int) file.length()];
bis.read(bytes);
bis.close();
return bytes;
}
}InsertInstructionTransformer implements the interface ClassFileTransformer and provides an implementation of the transform method. While the class InsertInstructionTransformer has methods to obtain the bytes associated with a class, we are mostly interested in the following method
public byte[] transform(byte[] classfileBuffer) {
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
ClassAdapter ca = new InsertInstructionClassAdapter(cw);
cr.accept(ca, 0);
return cw.toByteArray();
}Given a class file as a byte array, we instance an instance of ClassReader which is part of the ASM byte code manipulation library. The ASM library provide easy mechanism for byte code manipulation. The API provide 2 ways to access class byte code, one uses a Tree API and the other a Visitor design pattern. The following code uses the Visitor design pattern. Of interest to us is the implementation of InsertInstructionClassAdapter
class InsertInstructionClassAdapter extends ClassAdapter {
public InsertInstructionClassAdapter(ClassVisitor cv) {
super(cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature,
exceptions);
if ("timesTen".equals(name))
return new InsertInstructionMethodAdapter(mv);
return mv;
}
}InsertInstructionClassAdapter extends ClassAdapter and makes use of the method visitor return by the super implementation. The default implementation of Visitor interfaces in ASM do not provide for any manipulation, that is out responsibility. We wish to change the byte code of any method with the name
timesTenand return the method visitor implementation InsertInstructionMethodAdapter when a method with this name is encountered.
Let us examine the class InsertInstructionMethodAdapter, reproduced below
class InsertInstructionMethodAdapter extends MethodAdapter {
public InsertInstructionMethodAdapter(MethodVisitor mv) {
super(mv);
}
@Override
public void visitVarInsn(int opcode, int var) {
if (opcode == Opcodes.ILOAD && var == 1) {
// MULTIPLY VAR 1 (i) BY 2
mv.visitVarInsn(ILOAD, 1);
mv.visitIntInsn(BIPUSH, 2);
mv.visitInsn(IMUL);
// STORE VAR 1 (i)
mv.visitVarInsn(ISTORE, 1);
}
super.visitVarInsn(opcode, var);
}
}While the MethodVisitor provides for many methods to be overridden we override just the
visitVarInsnmethod. On the occurrence of a load Opcode with for the first method parameter we insert the following opcodes prior to passing in the original opcode the the parent method adapter.
// MULTIPLY VAR 1 (i) BY 2
mv.visitVarInsn(ILOAD, 1);
mv.visitIntInsn(BIPUSH, 2);
mv.visitInsn(IMUL);
// STORE VAR 1 (i)
mv.visitVarInsn(ISTORE, 1);
We load the first method parameter, push the constant 2 on to the stack, multiply the first 2 elements on the stack and store the result to the method parameter value. Looking at our Simple class, reproduced below
// CLIENT CLASS
public class Simple {
// modify this to be timesTwenty
public int timesTen(final int i) {
return i * 10;
}
public static void main(String[] args) {
Simple s = new Simple();
System.out.println(s.timesTen(2));
}
}
It is the equivalent of performing the following source code transformation
// CLIENT CLASS
public class Simple {
// modify this to be timesTwenty
public int timesTen(final int i) {
i *= 2;
return i * 10;
}
public static void main(String[] args) {
Simple s = new Simple();
System.out.println(s.timesTen(2));
}
}
Clearly this not something that the Java compiler would permit you to do, however running the code passes it through the byte code verifier that is part of the JVM and executes successfully with the result of multiplication by 20 rather than 10. Or put another way the JVM does not understand finality with reference to method parameters. Which begs the question, but why.
As any Java programmer would tell in java you maintain and pass around reference to object. Most will also tell you that Java (by extension the JVM) supports only call by value. Often not understood and lost in the trivial statement of fact is how the references interact when passed by value. It is the value of the reference that is passed to the invoked method, take a moment to read that again, value of the reference. Any changes to the value does not impact the original reference, only changes to the referenced object are visible outside of the method when it returns. Or, the JVM has no use for enforcing of method parameter finality.
[1] http://java.sun.com/docs/books/jls/third_edition/html/classes.html#38649
[2] http://java.sun.com/j2se/1.5.0/docs/api/java/lang/instrument/package-summary.html?PHPSESSID=99d1bf24adfefafb49c6f143c76dc59e
Often times they are added thru eclipse save actions.
ReplyDelete