JLS 15.12 (Method Invocation Expressions) in Plain English

This article aims at giving a more understandable description of what a method invocation expression really is. The Java Language Specification is targeted at people having a certain mathematical background. What we are trying to do here is to explain its content to people having a moderate experience in technical English and willing to get to know Java in a more advanced way without coping with pure maths.

There are a lot of things going on under the hood when methods are invoked. First of all, a method is to be seen as a service a class provides to itself and/or to other classes.

Whenever the compiler encounters a method invocation it has to figure out which method is to be invoked and most importantly where to find its implementation.

To get the ball rolling let's just think of some real-life examples. If we want to order a pizza, we have to call a pizza delivery place and not the immigration office or the hospital. If we want to buy a car, we sure don't go to Starbucks or the train station. Put shortly, we have to find the right place where we are sure we are going to get the service we need.

"we" stands here for the compiler which is in charge of turning the human-readable Java code you have written into bytecode the Java Virtual Machine (JVM) will understand. And that's exactly how we are going to proceed here, we will try to "think" exactly the same way the compiler does. Trying to figure out what the compiler does and how it works is an excellent exercise not only to gain in-depth knowledge of the compilation process but also to spare development time by recognizing code that will result in compile-time and run-time errors.

The following table shows how this article is organized. In order to keep things pretty much organized the same way as in Section 15.12 of the the Java Language Specification (JLS), we decided to keep the same structure. The first part discusses the three steps performed at compile-time while the second focuses on what is achieved at run-time by the linker and interpreter.
Compile Time Step 1 Determine Class or Interface to Search
Step 2 Determine Method Signature
Step 3 Is The Chosen Method Appropriate?
Run-time Step 4 Compute Target Reference (If Necessary)
Step 5 Evaluate Arguments
Step 6 Check Accessibility of Type and Method
Step 7 Locate Method to Invoke
Step 8 Create Frame, Synchronize, Transfer Control

Ok, let's go. Below is a slightly modified version of the method invocation expression given in JLS 15.12:
MethodInvocation:
                      Identifier (ArgumentListopt)
  TypeName  .         Identifier (ArgumentListopt)
  FieldName .         Identifier (ArgumentListopt)
  Primary   .         Identifier (ArgumentListopt)
  super     .         Identifier (ArgumentListopt)
  ClassName . super . Identifier (ArgumentListopt)

ArgumentList:
  Expression
  ArgumentList , Expression

Although those expressions may seem overly complex, they are not. We can see that the part after the left paranthesis, that is ArgumentListopt), is common to all four cases. By investigating the form BEFORE the left paranthesis, we notice that we always find an Identifier which may be qualified or not. To qualify a method means that we have a clue of where the method may reside and we want to specify it (with the dot-notation). So, we will look into those six different cases and explain when they are used and what purposes they serve.

Moreover, we are going to illustrate the concepts using the following example which contains at least one sample of each expression given above:

  1. public class MethodInvocation extends Object{
  2. public MethodInvocation(){
  3. }
  4. public void anInstanceMethod(){
  5. this.anotherInstanceMethod();
  6. aStaticMethod("test1");
  7. Class c = getClass();
  8. }
  9. public void anotherInstanceMethod(){
  10. }
  11. public static void aStaticMethod(String s){
  12. }
  13. public static MethodInvocation getMethodInvocationInstance(){
  14. return new MethodInvocation();
  15. }
  16. public String toString(){
  17. return super.toString();
  18. }
  19. public static void main(String args[]){
  20. aStaticMethod("test2");
  21. MethodInvocation.aStaticMethod("test3");
  22. MethodInvocation mi = getMethodInvocationInstance();
  23. mi.anotherInstanceMethod();
  24. getMethodInvocationInstance().anInstanceMethod();
  25. Runtime.getRuntime().freeMemory();
  26. System.exit(0);
  27. }
  28. }

Step 1. Compile-Time Step 1: Determine Class or Interface to Search

The compiler first has to discover the class --what we called a "place"-- in which the method --what we called a "service"-- we need is defined.

  1. The first form is the simplest of all and is used to: Identifier is the method name itself. It is a simple name being a valid Java identifier as explained in JLS 3.8 Identifiers.
    Lines 5, 6, 7, 20 and 22 above show that concept. Lines 5,6 and 20 contain invocations to methods defined in MethodInvocation whereas line 7 invokes getClass which is inherited from the superclass Object and line 22 invokes the static method getMethodInvocationInstance defined in the same class, that is MethodInvocation.
  2. In the second form, TypeName . Identifier, we say that the invocation of the method Identifier is qualified. TypeName is in fact the name of the class that contains the method Identifier. It is easy to see that this form is only used to invoke static methods and that a compile-time error occurs if TypeName is the name of an interface rather than a class since interfaces cannot have static methods.
    On line 21, we invoke the static method named aStaticMethod defined in the class MethodInvocation. Note that since the method aStaticMethod is in the same class as the invocation expression, it has the same functionality as the unqualified invocation on lines 6 and 20.
    On line 26, we invoke the static method exit defined in the class System. In this case, the qualified invocation expression is needed since we have to specify where the method called exit is to be found, that is in class System.
  3. In the third form, FieldName . Identifier, the invocation of the method Identifier is also qualified. FieldName is now the name of a reference to an object (an instance of a class). The class or interface to search is the declared type of the field named FieldName. This form is used to invoke a static or non-static method on a specific object.
    On line 23, we invoke the non-static method anotherInstanceMethod on the reference mi of type MethodInvocation.
    This invocation form may also be used to invoke a static method and the type of FieldName will be used to determine the class as it was the case in the second form. This is bad practice as far as readability and consistence are concerned, though. Basically, one should use the second form to invoke static methods and the third form to invoke non-static methods.
    Note: Readability sure is a matter of personal taste. But the bottom line is that it is much better to settle for some kind of consistence while coding, that is invoke instance methods declared in the same class using the keyword this (as on line 5) and invoke static method declared in the same class using the class name (as on line 21). That way developers will grasp much more quickly the intent of your code and won't need to search for the place where the method is declared.

  4. In the fourth form, Primary . Identifier, the invocation of the method Identifier is qualified as well. Primary is now an expression whose type is used to determine the class or interface providing the implementation of the method Identifier. This form is only used to invoke non-static methods.
    Line 24 contains two different method invocation expressions. The one on the left side, getMethodInvocationInstance, is what we call the Primary expression here (you may notice that the method is invoked using the very first form we have discussed above). Then, the return type of that method (MethodInvocation) is used to determine the class or interface which provides the implementation of the method on the right side, anInstanceMethod. Put shortly, getMethodInvocationInstance is invoked and returns an instance of the class MethodInvocation upon which the method anInstanceMethod will then be invoked.
    Be aware that Primary may be a complex expression, that is an expression chain as on line 25. The most important thing to know is that the evaluation goes from left to right.
  5. In the fifth form, super . Identifier, Identifier is the name of the method to invoke and it can be found in the superclass of the class containing the invocation. This form is used to invoke static and non-static methods defined in the superclass of the class containing the invocation.
    From this, we can logically derive that two error cases may arise, namely when the class containing the invocation is the class Object (Object has no superclass) or when the invocation is contained within an interface (interfaces do not contain any implementation).
    Why do we need this invocation pattern if methods (except private) are inherited anyway? Well, the case may arise where you may want to override (an instance method) or hide (a static method) declared in the superclass in order to provide your class with a more specific behavior.
    Line 16 to 18 show the concept. The class MethodInvocation contains an overridden declaration of the toString method originally declared in class Object. But for now we are happy with the default implementation of method toString and decide to rely on the behavior provided by class Object until our needs evolve. Thus, we invoke toString of class Object using the prefix super and return the result.
    As a rule of thumb, as soon as you override and/or hide a method and you want to invoke the overridden/hidden method, you'll need to prefix the method name with super.. This way you bypass the overriding method in your class.
  6. The last form, ClassName . super . Identifier, is used when dealing with inner classes. The name of the method is Identifier and it is to be found in the superclass of the class ClassName. The next requirement is that ClassName MUST be a lexically enclosing class of the class containing the invocation (in brief it means that the class containing the invocation is a member class of ClassName, see JLS 8.1.2), otherwise a compile-time error occurs. A compile-time error occurs as well if the class in which the invocation occurs is Object or an interface (for the same reason stated in the fifth form).

The goal of this first step was to determine the class or interface which contains the method to invoke. Now that we know exactly where to search we have to look for the right method to invoke. This is done in the second step where we will investigate which method signature fits best the invocation expression.

In the rest of this article, we will use the same code examples as in the JLS.

Step 2. Compile-Time Step 2: Determine Method Signature

The choice of the right method is based on the method descriptor.

Note: Method signature? Method descriptor? What are they? The method signature is composed of the method name and the parameter list (ordered!). The method descriptor takes the method signature AND the return type.

This step consists of two sub-steps:
  1. we have to find the method declarations that are both applicable and accessible
  2. we have to find the most specific method since several method declarations may meet the previous requirement.

Applicability

Two conditions must be met for a method declaration to be applicable:

The class or interface we found in the previous step as well as all its superclasses and superinterfaces are searched for all method declarations applicable to the method invocation we are trying to resolve.

Accessibility

Accessibility depends on what type of access the method has been granted (public, none, protected or private) and where the invocation expression appears. See Section 6.6 (Access Control) of the JLS for specific details concerning accessibility.

To get a better understanding of the applicability and accessibility concepts, let's have a look at the following code borrowed from JLS 15.12.2.1 (Find Methods that are Applicable and Accessible):
  1. public class Doubler {
  2. static int two() {return two(1);}
  3. private static int two(int i) {return 2*i;}
  4. }
  5. class Test extends Doubler {
  6. public static long two(long j) {return j+j;}
  7. public static void main(String[] args) {
  8. System.out.println(two(3));
  9. System.out.println(Doubler.two(3)); //compile-time error
  10. }
  11. }

If we don't manage to find any method declaration that is both applicable and accessible, then an error during the compilation process will result as it is the case on line 9.

Locate the Most Specific Method

The case where several methods are applicable and accessible arises while overloading methods. What do we do then? We just have to choose the most specific one. How do we do that?
The JLS says:

The informal intuition is that one method declaration is more specific than another if any invocation handled by the first method could be passed on to the other one without a compile-time type error.

To see how this makes sense, let's take a look at the following code. We have:
public void doSomeJob(String s)
and
public void doSomeJob(Object o)
Both methods are overloaded (same name but different parameter types). Now, suppose we have the following invocation expression:
doSomeJob("Test");
Both methods are applicable since "Test" is a String but also an Object (since a String is an Object). Always come to think of the most specific method as the one having its parameter types matching the best the argument types of the invocation. So, the first method is more specific than the second one because its parameter (String) can be converted to the second's parameter (Object) by method invocation conversion (see JLS 5.3).

As we have seen above, we may end up with several methods being accessible and applicable. The final goal of this step is to find the maximally specific method. Put simply, a method is maximally specific if it is applicable and accessible and there is no other applicable and accessible method that is more specific.
Now, as the flowchart below will show, it is possible to reach different verdicts:

Another important consideration is that the return type of the method is not taken into account when resolving method declarations. The following example illustrates the problem:
  1. class Point { int x, y; }
  2. class ColoredPoint extends Point { int color; }
  3. class Test {
  4. static int test(ColoredPoint p) {
  5. return p.color;
  6. }
  7. static String test(Point p) {
  8. return "Point";
  9. }
  10. public static void main(String[] args) {
  11. ColoredPoint cp = new ColoredPoint();
  12. String s = test(cp); //compile-time error
  13. }
  14. }

The invocation expression on line 12 will cause a compile-time error. The method on line 4 is the most specific (since it takes a ColoredPoint argument) but the problem is that the return type, int, is not assignment compatible with a String. The method on line 7 is also applicable but less specific than the method on line 4. Moreover, by choosing the method on line 7 the compilation would succeed because of the return type, String.
Trick : However, you could make this code compilable by changing line 12 to:
String s = ""+test(cp);
This way the returned int will be transformed to a String object.

Step 3. Compile-Time Step 3: Is The Chosen Method Appropriate?

If we managed to get to this step, it means that we have found THE most specific method matching the invocation expression and we call it the compile-time declaration for the method invocation. Now what? Remember the invocation patterns we saw at step 1? We now have to check if the chosen method can be used in the given context, that is, at the place where the invocation occurs. As an example, you certainly already know that you cannot invoke an instance method from within the body of a static method. We have to make sure that such things don't happen. This is what this step is for.

If the invocation pattern is:


Moreover, if the return type of the compile-time declaration is void then the method cannot be used where a value is expected. For instance, suppose we have the following expression:
String s = doSomething();
and doSomething's return type is void then a compile-time error occurs.
More generally, a method whose return type is void can only be used as an expression statement, that is, alone on a line, or in the initialization or update part of a for statement (see JLS 14.13).

Finally, if the chosen method passes all tests successfully, the compilation will succeeds and we can proceed to runtime checks. The bytecode of the method invocation now contains the following information that can be used at runtime by the Java bytecode interpreter:

Step 4. Runtime Step 1: Compute Target Reference (If Necessary)

A target reference is a reference to an instance of a class (an object) on which the method will be invoked. No target references are needed for static methods, that is, methods whose invocation mode is static.
The target reference is computed differently depending on the invocation pattern.
If the invocation expression is:

Step 5. Runtime Step 2: Evaluate Arguments

This step's job is to evaluate the argument expression from left to right. If any argument expression evaluation fails, then no argument expression on its right will be evaluated and the method invocation expression fails for the same reason.

Step 6. Runtime Step 3: Check Accessibility of Type and Method

We now have to determine if the type of the target reference and the method to invoke are accessible. Three different entities are involved here:

Any Java programming language implementation must check during linkage that:

Accessibility is the next issue. Since the invocation of method m (declared in class/interface T) occurs in class C, the compiler has to make sure that:

  1. T is accessible from C, and then that
  2. m (in T) is also accessible from C.
In clear, this means that the class containing the method invocation must be able to access the class or interface containing the declaration of the method. If this access is granted, then the class containing the method invocation must also be able to access the method itself. Read carefully the latter two sentences again and you'll see that they make sense.

Class/interface T is accessible from class C if:
  • T and C are in the same package.
    (Two classes in the same package are always accessible to one another.)
  • T and C are in a different package and T is public.
    (If two classes are unrelated and belong to different packages, they are only accessible to each other if they are declared public.)
  • T and C are in a different package, T is protected and T is a superclass of C.
    (A protected class/interface is available outside a package only to a subclass/subinterface.)

Finally, method m (in T) is accessible from class C if either one of the following is true:

If this paragraph seems overly complex to you, rest assured that it wasn't out original intent. The content is very logical. Just read it once or twice before going on. Think of these accessibility issues as a sort of labyrinth where not only one but several paths lead to the exit and some don't. Everything depends on the accessibility modifiers. The next picture will help you find your way through the labyrinth.

If either of the accessibility checks fails, then an IllegalAccessError occurs.

Step 7. Runtime Step 4: Locate Method to Invoke

We now have to locate the method to invoke. The invocation mode (step 3) provides us with that information. After this step we end up with a precise method to invoke. Don't worry if you feel lost after reading this paragraph, the meaning is not very easy to grasp and, moreover, this is the most important but also the most difficult step of the whole method invocation process. Read this section twice or thrice if you feel the need to do so.
Remember that we have 5 different invocation modes: static, nonvirtual, interface, super and virtual.

static
(static modifier)
We don't need any target reference (on which to invoke the method), overriding is NOT allowed and method m of class T is the one to be invoked. The method lookup ends here.
Note : Every subsequent invocation mode handles an instance method invocation, we therefore need the target reference on which the method will be invoked. If this reference is null then a NullPointerException is thrown. Otherwise, the target reference (see step 4) refers to the target object which will be used as this in the method body.
nonvirtual
(private modifier)
Overriding is NOT allowed and method m of class T is the one to be invoked. The method lookup ends here.

For the three remaining invocation modes, overriding may occur. That's why we need a dynamic method lookup process. The process starts looking for the method in class S (and then if needed in the superclasses of S), where S is the runtime class of the object on which the method is invoked. Moreover,

The method lookup for interface, virtual and super invocation mode is as follows:
If S contains a non-abstract declaration of a method named m having the same descriptor, that is, same number of parameters, same parameter type AND same return type, then:

Otherwise, if S has a superclass, then this lookup is performed recursively on the direct superclass of S. The result of this recursion will be the method to be invoked.

This lookup always succeeds provided that the compilation of the classes has been done in a consistent way or various errors may occur.

Step 8. Runtime Step 5: Create Frame, Synchronize, Transfer Control

This step just handles administration stuff, like creating a new activation frame, synchronizing data access if needed and transferring control to the method body.
We won't go into very much details here. If you want to know exactly what an activation frame is, or what synchonization means, please refer to the given hyperlinks.

Activation
Frame
Creation
At the end of the previous step, we ended up with the actual method to invoke. The only thing that remains to be done is to execute it. To do so, we create a new activation frame containing:
  • the target reference this (only for non-static methods) referring to the actual object on which the method is invoked;
  • the argument values (values passed between the paranthesis) if any;
  • enough space for local variables;
  • any other bookkeeping information (stack pointer, program counter, reference to previous activation frame, and the like);
At this point, if not enough memory is available to create the frame, then a OutOfMemoryError occurs.
This new activation frame becomes the new current activation frame and the argument values are assigned to the newly created parameter variables (available within the method's body). Just think of the activation frame as a context for the method execution that contains everything that the method may need to execute properly.
native
methods
If the method is declared native and the binary code for the method has not been loaded, then an UnsatisfiedLinkError occurs.
synchronized
methods
Finally, if the method is not synchronized, that is, its declaration does not contain the synchronized keyword, the control is transferred to the method's body. Otherwise, we have to lock an object before transferring the control to the method's body.
If the method is:
  • static, then the Class object of the class containing the method is locked;
  • non-static (an instance method), then the target reference (the object itself) is locked.
Just after the locking has occurred, the control is transferred to the method's body for further processing. The previously locked object is unlocked upon completion, whether normally or abruptly (in the case of exceptions or errors).

Written by Valentin Crettaz.