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:
|
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.
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
.
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.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
.
anotherInstanceMethod
on the reference mi
of type MethodInvocation
.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.
|
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.Object
(Object
has no superclass) or when the invocation is contained within an interface
(interfaces do not contain any implementation).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.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.
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. |
Two conditions must be met for a method declaration to be applicable:
double
if a String
is required,
for instance.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 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):
|
two
with one int
argument (1) from within class
Doubler
. We can see that
we have two methods named two
within class Doubler
but only the second
one (on line 3) is applicable because its parameter list matches the argument list of the invocation
expression on line 2. At runtime, the method named two
on line 3 will be invoked.two
with one int
argument (3) from within class
Test
. From the latter's perspective, we have three methods named two
but the one on line 2 is not applicable because the parameter list is empty. The one on
line 3 is applicable since it fulfills the applicability requirements stated above but it is not accessible
(private
accessibility) and thus cannot be used. The only applicable and accessible method is
the one on line 6 (Note that the argument 3 is converted to long
) within class Test
.private
modifier!!) method in class Doubler
.
The compiler will choke on this and spit some error stating that: two(int) has private access in Doubler
,
which is quite understandable.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.
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.
public void doSomeJob(String s)
public void doSomeJob(Object o)
doSomeJob("Test");
"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:
public void doSomeJob(String s, Object o)
public void doSomeJob(Object o, String s)
doSomeJob("test1","test2");
public void doSomeJob(String s1, String s2)
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:
|
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.
|
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:
this
is not available in static contexts).
static
otherwise a compile-time error occurs. The reason is because
an instance method must be invoked on an object and not on a class. (while a static method
can be invoked both on a class or on an instance as we will see later.)this
to refer to the current instance nor
a super
to refer to the superclass instance.)void
then the
method cannot be used where a value is expected. For instance, suppose we have the following
expression:String s = doSomething();
doSomething
's return type is void
then a compile-time error
occurs.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:
void
(note that void
IS NOT a
primitive type in Java,
see this
discussion at JavaRanch);static
: if the method declaration contains the static
modifier;non-virtual
: if the method is declared private
;super
: if the invocation pattern is super . Identifier or
ClassName . super . Identifier;interface
: if the method is declared in a interface;virtual
for any other cases.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:
static
, there is no need for a target reference.
That means that if a static
method is invoked upon a null
reference,
no NullPointerException
will be thrown.static
, there is no target reference.static
, there is no target reference. The
expression Primary is evaluated but the result is discarded anyway.this
.ClassName . this
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.
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:
m
to invoke;C
containing the invocation;T
in which m
is declared.Any Java programming language implementation must check during linkage that:
m
exists in class/interface T
, otherwise
a NoSuchMethodError
is thrown.interface
, then the target reference type
must implement the specified interface otherwise an IncompatibleClassChangeError
occurs.
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:
T
is accessible from C
, and then thatm
(in T
) is also accessible from C
.
Class/interface T
is accessible from class C
if:
|
|
|
|
|
Finally, method m
(in T
) is accessible from class C
if either one of the following is true:
m
is public
.m
is protected
, and either one of the following is true:
C
and T
are in the same package.protected
methods are available throughout a package.)C
and T
are the same class.protected
methods are available throughout a class.)C
is a subclass of T
.protected
methods are available to the defining class
and to all subclasses as well.)m
has default access (no modifier) and C
and
T
are in the same package.m
is private and either one of the following is true:
C
and T
are the same class.private
methods are available throughout a class.)C
encloses T
or T
encloses C
,
that is, one is an inner class of the other.private
methods are available to inner class.)C
and T
are both enclosed by a third class.private
methods are available to inner class.)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.
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,
interface
, then S
necessarly implements interface T
.virtual
, then S
is T
or a subclass of T
.
super
, then S
is the class/interface
in which the method is declared.
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:
interface
or super
, then m
is the method
to be invoked. The method lookup ends here.virtual
and the method declaration in S
overrides the one
in the compile-time type of the target reference of the method invocation, then m
is the method
to be invoked. The method lookup ends here.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.
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:
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:
|
Written by Valentin Crettaz.